summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/UnifiedComplete.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/places/UnifiedComplete.js')
-rw-r--r--toolkit/components/places/UnifiedComplete.js2149
1 files changed, 2149 insertions, 0 deletions
diff --git a/toolkit/components/places/UnifiedComplete.js b/toolkit/components/places/UnifiedComplete.js
new file mode 100644
index 000000000..ad3d35aab
--- /dev/null
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -0,0 +1,2149 @@
+/* -*- 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/. */
+
+"use strict";
+
+// Constants
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+// Match type constants.
+// These indicate what type of search function we should be using.
+const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
+const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE;
+const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
+const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING;
+const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE;
+
+const PREF_BRANCH = "browser.urlbar.";
+
+// Prefs are defined as [pref name, default value].
+const PREF_ENABLED = [ "autocomplete.enabled", true ];
+const PREF_AUTOFILL = [ "autoFill", true ];
+const PREF_AUTOFILL_TYPED = [ "autoFill.typed", true ];
+const PREF_AUTOFILL_SEARCHENGINES = [ "autoFill.searchEngines", false ];
+const PREF_RESTYLESEARCHES = [ "restyleSearches", false ];
+const PREF_DELAY = [ "delay", 50 ];
+const PREF_BEHAVIOR = [ "matchBehavior", MATCH_BOUNDARY_ANYWHERE ];
+const PREF_FILTER_JS = [ "filter.javascript", true ];
+const PREF_MAXRESULTS = [ "maxRichResults", 25 ];
+const PREF_RESTRICT_HISTORY = [ "restrict.history", "^" ];
+const PREF_RESTRICT_BOOKMARKS = [ "restrict.bookmark", "*" ];
+const PREF_RESTRICT_TYPED = [ "restrict.typed", "~" ];
+const PREF_RESTRICT_TAG = [ "restrict.tag", "+" ];
+const PREF_RESTRICT_SWITCHTAB = [ "restrict.openpage", "%" ];
+const PREF_RESTRICT_SEARCHES = [ "restrict.searces", "$" ];
+const PREF_MATCH_TITLE = [ "match.title", "#" ];
+const PREF_MATCH_URL = [ "match.url", "@" ];
+
+const PREF_SUGGEST_HISTORY = [ "suggest.history", true ];
+const PREF_SUGGEST_BOOKMARK = [ "suggest.bookmark", true ];
+const PREF_SUGGEST_OPENPAGE = [ "suggest.openpage", true ];
+const PREF_SUGGEST_HISTORY_ONLYTYPED = [ "suggest.history.onlyTyped", false ];
+const PREF_SUGGEST_SEARCHES = [ "suggest.searches", false ];
+
+const PREF_MAX_CHARS_FOR_SUGGEST = [ "maxCharsForSearchSuggestions", 20];
+
+// AutoComplete query type constants.
+// Describes the various types of queries that we can process rows for.
+const QUERYTYPE_FILTERED = 0;
+const QUERYTYPE_AUTOFILL_HOST = 1;
+const QUERYTYPE_AUTOFILL_URL = 2;
+
+// This separator is used as an RTL-friendly way to split the title and tags.
+// It can also be used by an nsIAutoCompleteResult consumer to re-split the
+// "comment" back into the title and the tag.
+const TITLE_TAGS_SEPARATOR = " \u2013 ";
+
+// Telemetry probes.
+const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
+const TELEMETRY_6_FIRST_RESULTS = "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS";
+// The default frecency value used when inserting matches with unknown frecency.
+const FRECENCY_DEFAULT = 1000;
+
+// Remote matches are appended when local matches are below a given frecency
+// threshold (FRECENCY_DEFAULT) as soon as they arrive. However we'll
+// always try to have at least MINIMUM_LOCAL_MATCHES local matches.
+const MINIMUM_LOCAL_MATCHES = 6;
+
+// Extensions are allowed to add suggestions if they have registered a keyword
+// with the omnibox API. This is the maximum number of suggestions an extension
+// is allowed to add for a given search string.
+const MAXIMUM_ALLOWED_EXTENSION_MATCHES = 6;
+
+// A regex that matches "single word" hostnames for whitelisting purposes.
+// The hostname will already have been checked for general validity, so we
+// don't need to be exhaustive here, so allow dashes anywhere.
+const REGEXP_SINGLEWORD_HOST = new RegExp("^[a-z0-9-]+$", "i");
+
+// Regex used to match userContextId.
+const REGEXP_USER_CONTEXT_ID = /(?:^| )user-context-id:(\d+)/;
+
+// Regex used to match one or more whitespace.
+const REGEXP_SPACES = /\s+/;
+
+// Sqlite result row index constants.
+const QUERYINDEX_QUERYTYPE = 0;
+const QUERYINDEX_URL = 1;
+const QUERYINDEX_TITLE = 2;
+const QUERYINDEX_ICONURL = 3;
+const QUERYINDEX_BOOKMARKED = 4;
+const QUERYINDEX_BOOKMARKTITLE = 5;
+const QUERYINDEX_TAGS = 6;
+const QUERYINDEX_VISITCOUNT = 7;
+const QUERYINDEX_TYPED = 8;
+const QUERYINDEX_PLACEID = 9;
+const QUERYINDEX_SWITCHTAB = 10;
+const QUERYINDEX_FRECENCY = 11;
+
+// 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, ', ')
+ 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 "btitle"
+// and "tags" queries for bookmarked entries.
+function defaultQuery(conditions = "") {
+ let query =
+ `SELECT :query_type, h.url, h.title, f.url, ${SQL_BOOKMARK_TAGS_FRAGMENT},
+ h.visit_count, h.typed, h.id, t.open_count, h.frecency
+ FROM moz_places h
+ LEFT JOIN moz_favicons f ON f.id = h.favicon_id
+ LEFT JOIN moz_openpages_temp t
+ ON t.url = h.url
+ AND t.userContextId = :userContextId
+ WHERE h.frecency <> 0
+ AND AUTOCOMPLETE_MATCH(:searchString, h.url,
+ CASE WHEN bookmarked THEN
+ IFNULL(btitle, h.title)
+ ELSE h.title END,
+ CASE WHEN bookmarked THEN
+ tags
+ ELSE '' END,
+ h.visit_count, h.typed,
+ bookmarked, t.open_count,
+ :matchBehavior, :searchBehavior)
+ ${conditions}
+ ORDER BY h.frecency DESC, h.id DESC
+ LIMIT :maxResults`;
+ return query;
+}
+
+const SQL_SWITCHTAB_QUERY =
+ `SELECT :query_type, t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ t.open_count, NULL
+ 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
+ AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL,
+ NULL, NULL, NULL, t.open_count,
+ :matchBehavior, :searchBehavior)
+ ORDER BY t.ROWID DESC
+ LIMIT :maxResults`;
+
+const SQL_ADAPTIVE_QUERY =
+ `/* do not warn (bug 487789) */
+ SELECT :query_type, h.url, h.title, f.url, ${SQL_BOOKMARK_TAGS_FRAGMENT},
+ h.visit_count, h.typed, h.id, t.open_count, h.frecency
+ FROM (
+ SELECT ROUND(MAX(use_count) * (1 + (input = :search_string)), 1) AS rank,
+ place_id
+ FROM moz_inputhistory
+ WHERE input BETWEEN :search_string AND :search_string || X'FFFF'
+ GROUP BY place_id
+ ) AS i
+ JOIN moz_places h ON h.id = i.place_id
+ LEFT JOIN moz_favicons f ON f.id = h.favicon_id
+ LEFT JOIN moz_openpages_temp t
+ ON t.url = h.url
+ AND t.userContextId = :userContextId
+ WHERE AUTOCOMPLETE_MATCH(NULL, h.url,
+ IFNULL(btitle, h.title), tags,
+ h.visit_count, h.typed, bookmarked,
+ t.open_count,
+ :matchBehavior, :searchBehavior)
+ ORDER BY rank DESC, h.frecency DESC`;
+
+
+function hostQuery(conditions = "") {
+ let query =
+ `/* do not warn (bug NA): not worth to index on (typed, frecency) */
+ SELECT :query_type, host || '/', IFNULL(prefix, '') || host || '/',
+ ( SELECT f.url FROM moz_favicons f
+ JOIN moz_places h ON h.favicon_id = f.id
+ WHERE rev_host = get_unreversed_host(host || '.') || '.'
+ OR rev_host = get_unreversed_host(host || '.') || '.www.'
+ ) AS favicon_url,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, frecency
+ FROM moz_hosts
+ WHERE host BETWEEN :searchString AND :searchString || X'FFFF'
+ AND frecency <> 0
+ ${conditions}
+ ORDER BY frecency DESC
+ LIMIT 1`;
+ return query;
+}
+
+const SQL_HOST_QUERY = hostQuery();
+
+const SQL_TYPED_HOST_QUERY = hostQuery("AND typed = 1");
+
+function bookmarkedHostQuery(conditions = "") {
+ let query =
+ `/* do not warn (bug NA): not worth to index on (typed, frecency) */
+ SELECT :query_type, host || '/', IFNULL(prefix, '') || host || '/',
+ ( SELECT f.url FROM moz_favicons f
+ JOIN moz_places h ON h.favicon_id = f.id
+ WHERE rev_host = get_unreversed_host(host || '.') || '.'
+ OR rev_host = get_unreversed_host(host || '.') || '.www.'
+ ) AS favicon_url,
+ ( SELECT foreign_count > 0 FROM moz_places
+ WHERE rev_host = get_unreversed_host(host || '.') || '.'
+ OR rev_host = get_unreversed_host(host || '.') || '.www.'
+ ) AS bookmarked, NULL, NULL, NULL, NULL, NULL, NULL, frecency
+ FROM moz_hosts
+ WHERE host BETWEEN :searchString AND :searchString || X'FFFF'
+ AND bookmarked
+ AND frecency <> 0
+ ${conditions}
+ ORDER BY frecency DESC
+ LIMIT 1`;
+ return query;
+}
+
+const SQL_BOOKMARKED_HOST_QUERY = bookmarkedHostQuery();
+
+const SQL_BOOKMARKED_TYPED_HOST_QUERY = bookmarkedHostQuery("AND typed = 1");
+
+function urlQuery(conditions = "") {
+ return `/* do not warn (bug no): cannot use an index to sort */
+ SELECT :query_type, h.url, NULL, f.url AS favicon_url,
+ foreign_count > 0 AS bookmarked,
+ NULL, NULL, NULL, NULL, NULL, NULL, h.frecency
+ FROM moz_places h
+ LEFT JOIN moz_favicons f ON h.favicon_id = f.id
+ WHERE (rev_host = :revHost OR rev_host = :revHost || "www.")
+ AND h.frecency <> 0
+ AND fixup_url(h.url) BETWEEN :searchString AND :searchString || X'FFFF'
+ ${conditions}
+ ORDER BY h.frecency DESC, h.id DESC
+ LIMIT 1`;
+}
+
+const SQL_URL_QUERY = urlQuery();
+
+const SQL_TYPED_URL_QUERY = urlQuery("AND h.typed = 1");
+
+// TODO (bug 1045924): use foreign_count once available.
+const SQL_BOOKMARKED_URL_QUERY = urlQuery("AND bookmarked");
+
+const SQL_BOOKMARKED_TYPED_URL_QUERY = urlQuery("AND bookmarked AND h.typed = 1");
+
+// Getters
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
+ "resource://gre/modules/TelemetryStopwatch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSearchHandler",
+ "resource://gre/modules/ExtensionSearchHandler.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesSearchAutocompleteProvider",
+ "resource://gre/modules/PlacesSearchAutocompleteProvider.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesRemoteTabsAutocompleteProvider",
+ "resource://gre/modules/PlacesRemoteTabsAutocompleteProvider.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "textURIService",
+ "@mozilla.org/intl/texttosuburi;1",
+ "nsITextToSubURI");
+
+/**
+ * Storage object for switch-to-tab entries.
+ * This takes care of caching and registering open pages, that will be reused
+ * by switch-to-tab queries. It has an internal cache, so that the Sqlite
+ * store is lazy initialized only on first use.
+ * It has a simple API:
+ * initDatabase(conn): initializes the temporary Sqlite entities to store data
+ * add(uri): adds a given nsIURI to the store
+ * delete(uri): removes a given nsIURI from the store
+ * shutdown(): stops storing data to Sqlite
+ */
+XPCOMUtils.defineLazyGetter(this, "SwitchToTabStorage", () => Object.seal({
+ _conn: null,
+ // Temporary queue used while the database connection is not available.
+ _queue: new Map(),
+ initDatabase: Task.async(function* (conn) {
+ // To reduce IO use an in-memory table for switch-to-tab tracking.
+ // Note: this should be kept up-to-date with the definition in
+ // nsPlacesTables.h.
+ yield conn.execute(
+ `CREATE TEMP TABLE moz_openpages_temp (
+ url TEXT,
+ userContextId INTEGER,
+ open_count INTEGER,
+ PRIMARY KEY (url, userContextId)
+ )`);
+
+ // Note: this should be kept up-to-date with the definition in
+ // nsPlacesTriggers.h.
+ yield conn.execute(
+ `CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger
+ AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW
+ WHEN NEW.open_count = 0
+ BEGIN
+ DELETE FROM moz_openpages_temp
+ WHERE url = NEW.url
+ AND userContextId = NEW.userContextId;
+ END`);
+
+ this._conn = conn;
+
+ // Populate the table with the current cache contents...
+ for (let [userContextId, uris] of this._queue) {
+ for (let uri of uris) {
+ this.add(uri, userContextId);
+ }
+ }
+
+ // ...then clear it to avoid double additions.
+ this._queue.clear();
+ }),
+
+ add(uri, userContextId) {
+ if (!this._conn) {
+ if (!this._queue.has(userContextId)) {
+ this._queue.set(userContextId, new Set());
+ }
+ this._queue.get(userContextId).add(uri);
+ return;
+ }
+ this._conn.executeCached(
+ `INSERT OR REPLACE INTO moz_openpages_temp (url, userContextId, open_count)
+ VALUES ( :url,
+ :userContextId,
+ IFNULL( ( SELECT open_count + 1
+ FROM moz_openpages_temp
+ WHERE url = :url
+ AND userContextId = :userContextId ),
+ 1
+ )
+ )`
+ , { url: uri.spec, userContextId });
+ },
+
+ delete(uri, userContextId) {
+ if (!this._conn) {
+ // This should not happen.
+ if (!this._queue.has(userContextId)) {
+ throw new Error("Unknown userContextId!");
+ }
+
+ this._queue.get(userContextId).delete(uri);
+ if (this._queue.get(userContextId).size == 0) {
+ this._queue.delete(userContextId);
+ }
+ return;
+ }
+ this._conn.executeCached(
+ `UPDATE moz_openpages_temp
+ SET open_count = open_count - 1
+ WHERE url = :url
+ AND userContextId = :userContextId`
+ , { url: uri.spec, userContextId });
+ },
+
+ shutdown: function () {
+ this._conn = null;
+ this._queue.clear();
+ }
+}));
+
+/**
+ * This helper keeps track of preferences and keeps their values up-to-date.
+ */
+XPCOMUtils.defineLazyGetter(this, "Prefs", () => {
+ let prefs = new Preferences(PREF_BRANCH);
+ let types = ["History", "Bookmark", "Openpage", "Searches"];
+
+ function syncEnabledPref() {
+ loadSyncedPrefs();
+
+ let suggestPrefs = [
+ PREF_SUGGEST_HISTORY,
+ PREF_SUGGEST_BOOKMARK,
+ PREF_SUGGEST_OPENPAGE,
+ PREF_SUGGEST_SEARCHES,
+ ];
+
+ if (store.enabled) {
+ // If the autocomplete preference is active, set to default value all suggest
+ // preferences only if all of them are false.
+ if (types.every(type => store["suggest" + type] == false)) {
+ for (let type of suggestPrefs) {
+ prefs.set(...type);
+ }
+ }
+ } else {
+ // If the preference was deactivated, deactivate all suggest preferences.
+ for (let type of suggestPrefs) {
+ prefs.set(type[0], false);
+ }
+ }
+ }
+
+ function loadSyncedPrefs () {
+ store.enabled = prefs.get(...PREF_ENABLED);
+ store.suggestHistory = prefs.get(...PREF_SUGGEST_HISTORY);
+ store.suggestBookmark = prefs.get(...PREF_SUGGEST_BOOKMARK);
+ store.suggestOpenpage = prefs.get(...PREF_SUGGEST_OPENPAGE);
+ store.suggestTyped = prefs.get(...PREF_SUGGEST_HISTORY_ONLYTYPED);
+ store.suggestSearches = prefs.get(...PREF_SUGGEST_SEARCHES);
+ }
+
+ function loadPrefs(subject, topic, data) {
+ if (data) {
+ // Synchronize suggest.* prefs with autocomplete.enabled.
+ if (data == PREF_BRANCH + PREF_ENABLED[0]) {
+ syncEnabledPref();
+ } else if (data.startsWith(PREF_BRANCH + "suggest.")) {
+ loadSyncedPrefs();
+ prefs.set(PREF_ENABLED[0], types.some(type => store["suggest" + type]));
+ }
+ }
+
+ store.enabled = prefs.get(...PREF_ENABLED);
+ store.autofill = prefs.get(...PREF_AUTOFILL);
+ store.autofillTyped = prefs.get(...PREF_AUTOFILL_TYPED);
+ store.autofillSearchEngines = prefs.get(...PREF_AUTOFILL_SEARCHENGINES);
+ store.restyleSearches = prefs.get(...PREF_RESTYLESEARCHES);
+ store.delay = prefs.get(...PREF_DELAY);
+ store.matchBehavior = prefs.get(...PREF_BEHAVIOR);
+ store.filterJavaScript = prefs.get(...PREF_FILTER_JS);
+ store.maxRichResults = prefs.get(...PREF_MAXRESULTS);
+ store.restrictHistoryToken = prefs.get(...PREF_RESTRICT_HISTORY);
+ store.restrictBookmarkToken = prefs.get(...PREF_RESTRICT_BOOKMARKS);
+ store.restrictTypedToken = prefs.get(...PREF_RESTRICT_TYPED);
+ store.restrictTagToken = prefs.get(...PREF_RESTRICT_TAG);
+ store.restrictOpenPageToken = prefs.get(...PREF_RESTRICT_SWITCHTAB);
+ store.restrictSearchesToken = prefs.get(...PREF_RESTRICT_SEARCHES);
+ store.matchTitleToken = prefs.get(...PREF_MATCH_TITLE);
+ store.matchURLToken = prefs.get(...PREF_MATCH_URL);
+ store.suggestHistory = prefs.get(...PREF_SUGGEST_HISTORY);
+ store.suggestBookmark = prefs.get(...PREF_SUGGEST_BOOKMARK);
+ store.suggestOpenpage = prefs.get(...PREF_SUGGEST_OPENPAGE);
+ store.suggestTyped = prefs.get(...PREF_SUGGEST_HISTORY_ONLYTYPED);
+ store.suggestSearches = prefs.get(...PREF_SUGGEST_SEARCHES);
+ store.maxCharsForSearchSuggestions = prefs.get(...PREF_MAX_CHARS_FOR_SUGGEST);
+ store.keywordEnabled = true;
+ try {
+ store.keywordEnabled = Services.prefs.getBoolPref("keyword.enabled");
+ } catch (ex) {}
+
+ // If history is not set, onlyTyped value should be ignored.
+ if (!store.suggestHistory) {
+ store.suggestTyped = false;
+ }
+ store.defaultBehavior = types.concat("Typed").reduce((memo, type) => {
+ let prefValue = store["suggest" + type];
+ return memo | (prefValue &&
+ Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()]);
+ }, 0);
+
+ // Further restrictions to apply for "empty searches" (i.e. searches 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.
+ store.emptySearchDefaultBehavior = Ci.mozIPlacesAutoComplete.BEHAVIOR_RESTRICT;
+ if (store.suggestHistory) {
+ store.emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY |
+ Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED;
+ } else if (store.suggestBookmark) {
+ store.emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
+ } else {
+ store.emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE;
+ }
+
+ // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE.
+ if (store.matchBehavior != MATCH_ANYWHERE &&
+ store.matchBehavior != MATCH_BOUNDARY &&
+ store.matchBehavior != MATCH_BEGINNING) {
+ store.matchBehavior = MATCH_BOUNDARY_ANYWHERE;
+ }
+
+ store.tokenToBehaviorMap = new Map([
+ [ store.restrictHistoryToken, "history" ],
+ [ store.restrictBookmarkToken, "bookmark" ],
+ [ store.restrictTagToken, "tag" ],
+ [ store.restrictOpenPageToken, "openpage" ],
+ [ store.matchTitleToken, "title" ],
+ [ store.matchURLToken, "url" ],
+ [ store.restrictTypedToken, "typed" ],
+ [ store.restrictSearchesToken, "searches" ],
+ ]);
+ }
+
+ let store = {
+ _ignoreNotifications: false,
+ observe(subject, topic, data) {
+ // Avoid re-entrancy when flipping linked preferences.
+ if (this._ignoreNotifications)
+ return;
+ this._ignoreNotifications = true;
+ loadPrefs(subject, topic, data);
+ this._ignoreNotifications = false;
+ },
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference ])
+ };
+
+ // Synchronize suggest.* prefs with autocomplete.enabled at initialization
+ syncEnabledPref();
+
+ loadPrefs();
+ prefs.observe("", store);
+ Services.prefs.addObserver("keyword.enabled", store, true);
+
+ return Object.seal(store);
+});
+
+// Helper functions
+
+/**
+ * Used to unescape encoded URI strings and drop information that we do not
+ * care about.
+ *
+ * @param spec
+ * The text to unescape and modify.
+ * @return the modified spec.
+ */
+function fixupSearchText(spec) {
+ return textURIService.unEscapeURIForUI("UTF-8", stripPrefix(spec));
+}
+
+/**
+ * Generates the tokens used in searching from a given string.
+ *
+ * @param searchString
+ * The string to generate tokens from.
+ * @return an array of tokens.
+ * @note Calling split on an empty string will return an array containing one
+ * empty string. We don't want that, as it'll break our logic, so return
+ * an empty array then.
+ */
+function getUnfilteredSearchTokens(searchString) {
+ return searchString.length ? searchString.split(REGEXP_SPACES) : [];
+}
+
+/**
+ * Strip prefixes from the URI that we don't care about for searching.
+ *
+ * @param spec
+ * The text to modify.
+ * @return the modified spec.
+ */
+function stripPrefix(spec)
+{
+ ["http://", "https://", "ftp://"].some(scheme => {
+ // Strip protocol if not directly followed by a space
+ if (spec.startsWith(scheme) && spec[scheme.length] != " ") {
+ spec = spec.slice(scheme.length);
+ return true;
+ }
+ return false;
+ });
+
+ // Strip www. if not directly followed by a space
+ if (spec.startsWith("www.") && spec[4] != " ") {
+ spec = spec.slice(4);
+ }
+ return spec;
+}
+
+/**
+ * Strip http and trailing separators from a spec.
+ *
+ * @param spec
+ * The text to modify.
+ * @return the modified spec.
+ */
+function stripHttpAndTrim(spec) {
+ if (spec.startsWith("http://")) {
+ spec = spec.slice(7);
+ }
+ if (spec.endsWith("?")) {
+ spec = spec.slice(0, -1);
+ }
+ if (spec.endsWith("/")) {
+ spec = spec.slice(0, -1);
+ }
+ return spec;
+}
+
+/**
+ * Returns the key to be used for a URL in a map for the purposes of removing
+ * duplicate entries - any 2 URLs that should be considered the same should
+ * return the same key. For some moz-action URLs this will unwrap the params
+ * and return a key based on the wrapped URL.
+ */
+function makeKeyForURL(actionUrl) {
+ // At this stage we only consider moz-action URLs.
+ if (!actionUrl.startsWith("moz-action:")) {
+ return stripHttpAndTrim(actionUrl);
+ }
+ let [, type, params] = actionUrl.match(/^moz-action:([^,]+),(.*)$/);
+ try {
+ params = JSON.parse(params);
+ } catch (ex) {
+ // This is unexpected in this context, so just return the input.
+ return stripHttpAndTrim(actionUrl);
+ }
+ // For now we only handle these 2 action types and treat them as the same.
+ switch (type) {
+ case "remotetab":
+ case "switchtab":
+ if (params.url) {
+ return "moz-action:tab:" + stripHttpAndTrim(params.url);
+ }
+ break;
+ // TODO (bug 1222435) - "switchtab" should be handled as an "autofill"
+ // entry.
+ default:
+ // do nothing.
+ // TODO (bug 1222436) - extend this method so it can be used instead of
+ // the |placeId| that's also used to remove duplicate entries.
+ }
+ return stripHttpAndTrim(actionUrl);
+}
+
+/**
+ * Returns whether the passed in string looks like a url.
+ */
+function looksLikeUrl(str, ignoreAlphanumericHosts = false) {
+ // Single word not including special chars.
+ return !REGEXP_SPACES.test(str) &&
+ (["/", "@", ":", "["].some(c => str.includes(c)) ||
+ (ignoreAlphanumericHosts ? /(.*\..*){3,}/.test(str) : str.includes(".")));
+}
+
+/**
+ * Manages a single instance of an autocomplete search.
+ *
+ * The first three parameters all originate from the similarly named parameters
+ * of nsIAutoCompleteSearch.startSearch().
+ *
+ * @param searchString
+ * The search string.
+ * @param searchParam
+ * A space-delimited string of search parameters. The following
+ * parameters are supported:
+ * * enable-actions: Include "actions", such as switch-to-tab and search
+ * engine aliases, in the results.
+ * * disable-private-actions: The search is taking place in a private
+ * window outside of permanent private-browsing mode. The search
+ * should exclude privacy-sensitive results as appropriate.
+ * * private-window: The search is taking place in a private window,
+ * possibly in permanent private-browsing mode. The search
+ * should exclude privacy-sensitive results as appropriate.
+ * * user-context-id: The userContextId of the selected tab.
+ * @param autocompleteListener
+ * An nsIAutoCompleteObserver.
+ * @param resultListener
+ * An nsIAutoCompleteSimpleResultListener.
+ * @param autocompleteSearch
+ * An nsIAutoCompleteSearch.
+ * @param prohibitSearchSuggestions
+ * Whether search suggestions are allowed for this search.
+ */
+function Search(searchString, searchParam, autocompleteListener,
+ resultListener, autocompleteSearch, prohibitSearchSuggestions) {
+ // We want to store the original string for case sensitive searches.
+ this._originalSearchString = searchString;
+ this._trimmedOriginalSearchString = searchString.trim();
+ this._searchString = fixupSearchText(this._trimmedOriginalSearchString.toLowerCase());
+
+ this._matchBehavior = Prefs.matchBehavior;
+ // Set the default behavior for this search.
+ this._behavior = this._searchString ? Prefs.defaultBehavior
+ : Prefs.emptySearchDefaultBehavior;
+
+ let params = new Set(searchParam.split(" "));
+ this._enableActions = params.has("enable-actions");
+ this._disablePrivateActions = params.has("disable-private-actions");
+ this._inPrivateWindow = params.has("private-window");
+ this._prohibitAutoFill = params.has("prohibit-autofill");
+
+ let userContextId = searchParam.match(REGEXP_USER_CONTEXT_ID);
+ this._userContextId = userContextId ?
+ parseInt(userContextId[1], 10) :
+ Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
+
+ this._searchTokens =
+ this.filterTokens(getUnfilteredSearchTokens(this._searchString));
+ // The protocol and the host are lowercased by nsIURI, so it's fine to
+ // lowercase the typed prefix, to add it back to the results later.
+ this._strippedPrefix = this._trimmedOriginalSearchString.slice(
+ 0, this._trimmedOriginalSearchString.length - this._searchString.length
+ ).toLowerCase();
+ // The URIs in the database are fixed-up, so we can match on a lowercased
+ // host, but the path must be matched in a case sensitive way.
+ let pathIndex =
+ this._trimmedOriginalSearchString.indexOf("/", this._strippedPrefix.length);
+ this._autofillUrlSearchString = fixupSearchText(
+ this._trimmedOriginalSearchString.slice(0, pathIndex).toLowerCase() +
+ this._trimmedOriginalSearchString.slice(pathIndex)
+ );
+
+ this._prohibitSearchSuggestions = prohibitSearchSuggestions;
+
+ this._listener = autocompleteListener;
+ this._autocompleteSearch = autocompleteSearch;
+
+ // Create a new result to add eventual matches. Note we need a result
+ // regardless having matches.
+ let result = Cc["@mozilla.org/autocomplete/simple-result;1"]
+ .createInstance(Ci.nsIAutoCompleteSimpleResult);
+ result.setSearchString(searchString);
+ result.setListener(resultListener);
+ // Will be set later, if needed.
+ result.setDefaultIndex(-1);
+ this._result = result;
+
+ // These are used to avoid adding duplicate entries to the results.
+ this._usedURLs = new Set();
+ this._usedPlaceIds = new Set();
+
+ // Resolved when all the remote matches have been fetched.
+ this._remoteMatchesPromises = [];
+
+ // The index to insert remote matches at.
+ this._remoteMatchesStartIndex = 0;
+ // The index to insert local matches at.
+
+ this._localMatchesStartIndex = 0;
+
+ // Counts the number of inserted local matches.
+ this._localMatchesCount = 0;
+ // Counts the number of inserted remote matches.
+ this._remoteMatchesCount = 0;
+ // Counts the number of inserted extension matches.
+ this._extensionMatchesCount = 0;
+}
+
+Search.prototype = {
+ /**
+ * Enables the desired AutoComplete behavior.
+ *
+ * @param type
+ * The behavior type to set.
+ */
+ setBehavior: function (type) {
+ type = type.toUpperCase();
+ this._behavior |=
+ Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type];
+
+ // Setting the "typed" behavior should also set the "history" behavior.
+ if (type == "TYPED") {
+ this.setBehavior("history");
+ }
+ },
+
+ /**
+ * Determines if the specified AutoComplete behavior is set.
+ *
+ * @param aType
+ * The behavior type to test for.
+ * @return true if the behavior is set, false otherwise.
+ */
+ hasBehavior: function (type) {
+ let behavior = Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()];
+
+ if (this._disablePrivateActions &&
+ behavior == Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE) {
+ return false;
+ }
+
+ return this._behavior & behavior;
+ },
+
+ /**
+ * Used to delay the most complex queries, to save IO while the user is
+ * typing.
+ */
+ _sleepDeferred: null,
+ _sleep: function (aTimeMs) {
+ // Reuse a single instance to try shaving off some usless work before
+ // the first query.
+ if (!this._sleepTimer)
+ this._sleepTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._sleepDeferred = PromiseUtils.defer();
+ this._sleepTimer.initWithCallback(() => this._sleepDeferred.resolve(),
+ aTimeMs, Ci.nsITimer.TYPE_ONE_SHOT);
+ return this._sleepDeferred.promise;
+ },
+
+ /**
+ * Given an array of tokens, this function determines which query should be
+ * ran. It also removes any special search tokens.
+ *
+ * @param tokens
+ * An array of search tokens.
+ * @return the filtered list of tokens to search with.
+ */
+ filterTokens: function (tokens) {
+ let foundToken = false;
+ // Set the proper behavior while filtering tokens.
+ for (let i = tokens.length - 1; i >= 0; i--) {
+ let behavior = Prefs.tokenToBehaviorMap.get(tokens[i]);
+ // Don't remove the token if it didn't match, or if it's an action but
+ // actions are not enabled.
+ if (behavior && (behavior != "openpage" || this._enableActions)) {
+ // 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);
+ tokens.splice(i, 1);
+ }
+ }
+
+ // 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 (!Prefs.filterJavaScript) {
+ this.setBehavior("javascript");
+ }
+
+ return tokens;
+ },
+
+ /**
+ * 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() {
+ if (this._sleepTimer)
+ this._sleepTimer.cancel();
+ if (this._sleepDeferred) {
+ this._sleepDeferred.resolve();
+ this._sleepDeferred = null;
+ }
+ if (this._searchSuggestionController) {
+ this._searchSuggestionController.stop();
+ this._searchSuggestionController = null;
+ }
+ this.pending = false;
+ },
+
+ /**
+ * Whether this search is active.
+ */
+ pending: true,
+
+ /**
+ * Execute the search and populate results.
+ * @param conn
+ * The Sqlite connection.
+ */
+ execute: Task.async(function* (conn) {
+ // A search might be canceled before it starts.
+ if (!this.pending)
+ return;
+
+ TelemetryStopwatch.start(TELEMETRY_1ST_RESULT, this);
+ if (this._searchString)
+ TelemetryStopwatch.start(TELEMETRY_6_FIRST_RESULTS, this);
+
+ // Since we call the synchronous parseSubmissionURL function later, we must
+ // wait for the initialization of PlacesSearchAutocompleteProvider first.
+ yield PlacesSearchAutocompleteProvider.ensureInitialized();
+ if (!this.pending)
+ return;
+
+ // For any given search, we run many queries/heuristics:
+ // 1) by alias (as defined in SearchService)
+ // 2) inline completion from search engine resultDomains
+ // 3) inline completion for hosts (this._hostQuery) or urls (this._urlQuery)
+ // 4) directly typed in url (ie, can be navigated to as-is)
+ // 5) submission for the current search engine
+ // 6) Places keywords
+ // 7) adaptive learning (this._adaptiveQuery)
+ // 8) open pages not supported by history (this._switchToTabQuery)
+ // 9) query based on match behavior
+ //
+ // (6) only gets ran if we get any filtered tokens, since if there are no
+ // tokens, there is nothing to match. This is the *first* query we check if
+ // we want to run, but it gets queued to be run later.
+ //
+ // (1), (4), (5) only get run if actions are enabled. When actions are
+ // enabled, the first result is always a special result (resulting from one
+ // of the queries between (1) and (6) inclusive). As such, the UI is
+ // expected to auto-select the first result when actions are enabled. If the
+ // first result is an inline completion result, that will also be the
+ // default result and therefore be autofilled (this also happens if actions
+ // are not enabled).
+
+ // Get the final query, based on the tokens found in the search string.
+ let queries = [ this._adaptiveQuery ];
+
+ // "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);
+
+ // Add the first heuristic result, if any. Set _addingHeuristicFirstMatch
+ // to true so that when the result is added, "heuristic" can be included in
+ // its style.
+ this._addingHeuristicFirstMatch = true;
+ let hasHeuristic = yield this._matchFirstHeuristicResult(conn);
+ this._addingHeuristicFirstMatch = false;
+ if (!this.pending)
+ return;
+
+ // We sleep a little between adding the heuristicFirstMatch and matching
+ // any other searches so we aren't kicking off potentially expensive
+ // searches on every keystroke.
+ // Though, if there's no heuristic result, we start searching immediately,
+ // since autocomplete may be waiting for us.
+ if (hasHeuristic) {
+ yield this._sleep(Prefs.delay);
+ if (!this.pending)
+ return;
+ }
+
+ if (this._enableActions && this._searchTokens.length > 0) {
+ yield this._matchSearchSuggestions();
+ if (!this.pending)
+ return;
+ }
+
+ for (let [query, params] of queries) {
+ yield conn.executeCached(query, params, this._onResultRow.bind(this));
+ if (!this.pending)
+ return;
+ }
+
+ if (this._enableActions && this.hasBehavior("openpage")) {
+ yield this._matchRemoteTabs();
+ if (!this.pending)
+ return;
+ }
+
+ // If we do not have enough results, and our match type is
+ // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
+ // results.
+ if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE &&
+ this._localMatchesCount < Prefs.maxRichResults) {
+ this._matchBehavior = MATCH_ANYWHERE;
+ for (let [query, params] of [ this._adaptiveQuery,
+ this._searchQuery ]) {
+ yield conn.executeCached(query, params, this._onResultRow.bind(this));
+ if (!this.pending)
+ return;
+ }
+ }
+
+ // Only add extension suggestions if the first token is a registered keyword
+ // and the search string has characters after the first token.
+ if (ExtensionSearchHandler.isKeywordRegistered(this._searchTokens[0]) &&
+ this._originalSearchString.length > this._searchTokens[0].length) {
+ yield this._matchExtensionSuggestions();
+ if (!this.pending)
+ return;
+ } else if (ExtensionSearchHandler.hasActiveInputSession()) {
+ ExtensionSearchHandler.handleInputCancelled();
+ }
+
+ // Ensure to fill any remaining space. Suggestions which come from extensions are
+ // inserted at the beginning, so any suggestions
+ yield Promise.all(this._remoteMatchesPromises);
+ }),
+
+ *_matchFirstHeuristicResult(conn) {
+ // We always try to make the first result a special "heuristic" result. The
+ // heuristics below determine what type of result it will be, if any.
+
+ let hasSearchTerms = this._searchTokens.length > 0;
+
+ if (hasSearchTerms) {
+ // It may be a keyword registered by an extension.
+ let matched = yield this._matchExtensionHeuristicResult();
+ if (matched) {
+ return true;
+ }
+ }
+
+ if (this._enableActions && hasSearchTerms) {
+ // It may be a search engine with an alias - which works like a keyword.
+ let matched = yield this._matchSearchEngineAlias();
+ if (matched) {
+ return true;
+ }
+ }
+
+ if (this.pending && hasSearchTerms) {
+ // It may be a Places keyword.
+ let matched = yield this._matchPlacesKeyword();
+ if (matched) {
+ return true;
+ }
+ }
+
+ let shouldAutofill = this._shouldAutofill;
+ if (this.pending && shouldAutofill) {
+ // It may also look like a URL we know from the database.
+ let matched = yield this._matchKnownUrl(conn);
+ if (matched) {
+ return true;
+ }
+ }
+
+ if (this.pending && shouldAutofill) {
+ // Or it may look like a URL we know about from search engines.
+ let matched = yield this._matchSearchEngineUrl();
+ if (matched) {
+ return true;
+ }
+ }
+
+ if (this.pending && hasSearchTerms && this._enableActions) {
+ // If we don't have a result that matches what we know about, then
+ // we use a fallback for things we don't know about.
+
+ // We may not have auto-filled, but this may still look like a URL.
+ // However, even if the input is a valid URL, we may not want to use
+ // it as such. This can happen if the host would require whitelisting,
+ // but isn't in the whitelist.
+ let matched = yield this._matchUnknownUrl();
+ if (matched) {
+ // Since we can't tell if this is a real URL and
+ // whether the user wants to visit or search for it,
+ // we always provide an alternative searchengine match.
+ try {
+ new URL(this._originalSearchString);
+ } catch (ex) {
+ if (Prefs.keywordEnabled && !looksLikeUrl(this._originalSearchString, true)) {
+ this._addingHeuristicFirstMatch = false;
+ yield this._matchCurrentSearchEngine();
+ this._addingHeuristicFirstMatch = true;
+ }
+ }
+ return true;
+ }
+ }
+
+ if (this.pending && this._enableActions && this._originalSearchString) {
+ // When all else fails, and the search string is non-empty, we search
+ // using the current search engine.
+ let matched = yield this._matchCurrentSearchEngine();
+ if (matched) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ *_matchSearchSuggestions() {
+ // Limit the string sent for search suggestions to a maximum length.
+ let searchString = this._searchTokens.join(" ")
+ .substr(0, Prefs.maxCharsForSearchSuggestions);
+ // Avoid fetching suggestions if they are not required, private browsing
+ // mode is enabled, or the search string may expose sensitive information.
+ if (!this.hasBehavior("searches") || this._inPrivateWindow ||
+ this._prohibitSearchSuggestionsFor(searchString)) {
+ return;
+ }
+
+ this._searchSuggestionController =
+ PlacesSearchAutocompleteProvider.getSuggestionController(
+ searchString,
+ this._inPrivateWindow,
+ Prefs.maxRichResults,
+ this._userContextId
+ );
+ let promise = this._searchSuggestionController.fetchCompletePromise
+ .then(() => {
+ // The search has been canceled already.
+ if (!this._searchSuggestionController)
+ return;
+ if (this._searchSuggestionController.resultsCount >= 0 &&
+ this._searchSuggestionController.resultsCount < 2) {
+ // The original string is used to properly compare with the next search.
+ this._lastLowResultsSearchSuggestion = this._originalSearchString;
+ }
+ while (this.pending && this._remoteMatchesCount < Prefs.maxRichResults) {
+ let [match, suggestion] = this._searchSuggestionController.consume();
+ if (!suggestion)
+ break;
+ if (!looksLikeUrl(suggestion)) {
+ // Don't include the restrict token, if present.
+ let searchString = this._searchTokens.join(" ");
+ this._addSearchEngineMatch(match, searchString, suggestion);
+ }
+ }
+ });
+
+ if (this.hasBehavior("restrict")) {
+ // We're done if we're restricting to search suggestions.
+ yield promise;
+ this.stop();
+ } else {
+ this._remoteMatchesPromises.push(promise);
+ }
+ },
+
+ _prohibitSearchSuggestionsFor(searchString) {
+ if (this._prohibitSearchSuggestions)
+ return true;
+
+ // Suggestions for a single letter are unlikely to be useful.
+ if (searchString.length < 2)
+ return true;
+
+ // The first token may be a whitelisted host.
+ if (this._searchTokens.length == 1 &&
+ REGEXP_SINGLEWORD_HOST.test(this._searchTokens[0]) &&
+ Services.uriFixup.isDomainWhitelisted(this._searchTokens[0], -1)) {
+ return true;
+ }
+
+ // Disallow fetching search suggestions for strings looking like URLs, to
+ // avoid disclosing information about networks or passwords.
+ return this._searchTokens.some(looksLikeUrl);
+ },
+
+ _matchKnownUrl: function* (conn) {
+ // Hosts have no "/" in them.
+ let lastSlashIndex = this._searchString.lastIndexOf("/");
+ // Search only URLs if there's a slash in the search string...
+ if (lastSlashIndex != -1) {
+ // ...but not if it's exactly at the end of the search string.
+ if (lastSlashIndex < this._searchString.length - 1) {
+ // We don't want to execute this query right away because it needs to
+ // search the entire DB without an index, but we need to know if we have
+ // a result as it will influence other heuristics. So we guess by
+ // assuming that if we get a result from a *host* query and it *looks*
+ // like a URL, then we'll probably have a result.
+ let gotResult = false;
+ let [ query, params ] = this._urlQuery;
+ yield conn.executeCached(query, params, row => {
+ gotResult = true;
+ this._onResultRow(row);
+ });
+ return gotResult;
+ }
+ return false;
+ }
+
+ let gotResult = false;
+ let [ query, params ] = this._hostQuery;
+ yield conn.executeCached(query, params, row => {
+ gotResult = true;
+ this._onResultRow(row);
+ });
+ return gotResult;
+ },
+
+ _matchExtensionHeuristicResult: function* () {
+ if (ExtensionSearchHandler.isKeywordRegistered(this._searchTokens[0]) &&
+ this._originalSearchString.length > this._searchTokens[0].length) {
+ let description = ExtensionSearchHandler.getDescription(this._searchTokens[0]);
+ this._addExtensionMatch(this._originalSearchString, description);
+ return true;
+ }
+ return false;
+ },
+
+ _matchPlacesKeyword: function* () {
+ // The first word could be a keyword, so that's what we'll search.
+ let keyword = this._searchTokens[0];
+ let entry = yield PlacesUtils.keywords.fetch(this._searchTokens[0]);
+ if (!entry)
+ return false;
+
+ let searchString = this._trimmedOriginalSearchString.substr(keyword.length + 1);
+
+ let url = null, postData = null;
+ try {
+ [url, postData] =
+ yield BrowserUtils.parseUrlAndPostData(entry.url.href,
+ entry.postData,
+ searchString);
+ } catch (ex) {
+ // It's not possible to bind a param to this keyword.
+ return false;
+ }
+
+ let style = (this._enableActions ? "action " : "") + "keyword";
+ let actionURL = PlacesUtils.mozActionURI("keyword", {
+ url,
+ input: this._originalSearchString,
+ postData,
+ });
+ let value = this._enableActions ? actionURL : url;
+ // The title will end up being "host: queryString"
+ let comment = entry.url.host;
+
+ this._addMatch({ value, comment, style, frecency: FRECENCY_DEFAULT });
+ return true;
+ },
+
+ _matchSearchEngineUrl: function* () {
+ if (!Prefs.autofillSearchEngines)
+ return false;
+
+ let match = yield PlacesSearchAutocompleteProvider.findMatchByToken(
+ this._searchString);
+ if (!match)
+ return false;
+
+ // The match doesn't contain a 'scheme://www.' prefix, but since we have
+ // stripped it from the search string, here we could still be matching
+ // 'https://www.g' to 'google.com'.
+ // There are a couple cases where we don't want to match though:
+ //
+ // * If the protocol differs we should not match. For example if the user
+ // searched https we should not return http.
+ try {
+ let prefixURI = NetUtil.newURI(this._strippedPrefix);
+ let finalURI = NetUtil.newURI(match.url);
+ if (prefixURI.scheme != finalURI.scheme)
+ return false;
+ } catch (e) {}
+
+ // * If the user typed "www." but the final url doesn't have it, we
+ // should not match as well, the two urls may point to different pages.
+ if (this._strippedPrefix.endsWith("www.") &&
+ !stripHttpAndTrim(match.url).startsWith("www."))
+ return false;
+
+ let value = this._strippedPrefix + match.token;
+
+ // In any case, we should never arrive here with a value that doesn't
+ // match the search string. If this happens there is some case we
+ // are not handling properly yet.
+ if (!value.startsWith(this._originalSearchString)) {
+ Components.utils.reportError(`Trying to inline complete in-the-middle
+ ${this._originalSearchString} to ${value}`);
+ return false;
+ }
+
+ this._result.setDefaultIndex(0);
+ this._addMatch({
+ value: value,
+ comment: match.engineName,
+ icon: match.iconUrl,
+ style: "priority-search",
+ finalCompleteValue: match.url,
+ frecency: FRECENCY_DEFAULT
+ });
+ return true;
+ },
+
+ _matchSearchEngineAlias: function* () {
+ if (this._searchTokens.length < 1)
+ return false;
+
+ let alias = this._searchTokens[0];
+ let match = yield PlacesSearchAutocompleteProvider.findMatchByAlias(alias);
+ if (!match)
+ return false;
+
+ match.engineAlias = alias;
+ let query = this._trimmedOriginalSearchString.substr(alias.length + 1);
+
+ this._addSearchEngineMatch(match, query);
+ return true;
+ },
+
+ _matchCurrentSearchEngine: function* () {
+ let match = yield PlacesSearchAutocompleteProvider.getDefaultMatch();
+ if (!match)
+ return false;
+
+ let query = this._originalSearchString;
+ this._addSearchEngineMatch(match, query);
+ return true;
+ },
+
+ _addExtensionMatch(content, comment) {
+ if (this._extensionMatchesCount >= MAXIMUM_ALLOWED_EXTENSION_MATCHES) {
+ return;
+ }
+
+ this._addMatch({
+ value: PlacesUtils.mozActionURI("extension", {
+ content,
+ keyword: this._searchTokens[0]
+ }),
+ comment,
+ icon: "chrome://browser/content/extension.svg",
+ style: "action extension",
+ frecency: FRECENCY_DEFAULT,
+ extension: true,
+ });
+ },
+
+ _addSearchEngineMatch(match, query, suggestion) {
+ let actionURLParams = {
+ engineName: match.engineName,
+ input: suggestion || this._originalSearchString,
+ searchQuery: query,
+ };
+ if (suggestion)
+ actionURLParams.searchSuggestion = suggestion;
+ if (match.engineAlias) {
+ actionURLParams.alias = match.engineAlias;
+ }
+ let value = PlacesUtils.mozActionURI("searchengine", actionURLParams);
+
+ this._addMatch({
+ value: value,
+ comment: match.engineName,
+ icon: match.iconUrl,
+ style: "action searchengine",
+ frecency: FRECENCY_DEFAULT,
+ remote: !!suggestion
+ });
+ },
+
+ *_matchExtensionSuggestions() {
+ let promise = ExtensionSearchHandler.handleSearch(this._searchTokens[0], this._originalSearchString,
+ suggestions => {
+ suggestions.forEach(suggestion => {
+ let content = `${this._searchTokens[0]} ${suggestion.content}`;
+ this._addExtensionMatch(content, suggestion.description);
+ });
+ }
+ );
+ this._remoteMatchesPromises.push(promise);
+ },
+
+ *_matchRemoteTabs() {
+ let matches = yield PlacesRemoteTabsAutocompleteProvider.getMatches(this._originalSearchString);
+ for (let {url, title, icon, deviceName} of matches) {
+ // It's rare that Sync supplies the icon for the page (but if it does, it
+ // is a string URL)
+ if (!icon) {
+ try {
+ let favicon = yield PlacesUtils.promiseFaviconLinkUrl(url);
+ if (favicon) {
+ icon = favicon.spec;
+ }
+ } catch (ex) {} // no favicon for this URL.
+ } else {
+ icon = PlacesUtils.favicons
+ .getFaviconLinkForIcon(NetUtil.newURI(icon)).spec;
+ }
+
+ let match = {
+ // We include the deviceName in the action URL so we can render it in
+ // the URLBar.
+ value: PlacesUtils.mozActionURI("remotetab", { url, deviceName }),
+ comment: title || url,
+ style: "action remotetab",
+ // we want frecency > FRECENCY_DEFAULT so it doesn't get pushed out
+ // by "remote" matches.
+ frecency: FRECENCY_DEFAULT + 1,
+ icon,
+ }
+ this._addMatch(match);
+ }
+ },
+
+ // TODO (bug 1054814): Use visited URLs to inform which scheme to use, if the
+ // scheme isn't specificed.
+ _matchUnknownUrl: function* () {
+ let flags = Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
+ Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ let fixupInfo = null;
+ try {
+ fixupInfo = Services.uriFixup.getFixupURIInfo(this._originalSearchString,
+ flags);
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_MALFORMED_URI && !Prefs.keywordEnabled) {
+ let value = PlacesUtils.mozActionURI("visiturl", {
+ url: this._originalSearchString,
+ input: this._originalSearchString,
+ });
+ this._addMatch({
+ value,
+ comment: this._originalSearchString,
+ style: "action visiturl",
+ frecency: 0,
+ });
+
+ return true;
+ }
+ return false;
+ }
+
+ // If the URI cannot be fixed or the preferred URI would do a keyword search,
+ // that basically means this isn't useful to us. Note that
+ // fixupInfo.keywordAsSent will never be true if the keyword.enabled pref
+ // is false or there are no engines, so in that case we will always return
+ // a "visit".
+ if (!fixupInfo.fixedURI || fixupInfo.keywordAsSent)
+ return false;
+
+ let uri = fixupInfo.fixedURI;
+ // Check the host, as "http:///" is a valid nsIURI, but not useful to us.
+ // But, some schemes are expected to have no host. So we check just against
+ // schemes we know should have a host. This allows new schemes to be
+ // implemented without us accidentally blocking access to them.
+ let hostExpected = new Set(["http", "https", "ftp", "chrome", "resource"]);
+ if (hostExpected.has(uri.scheme) && !uri.host)
+ return false;
+
+ // getFixupURIInfo() escaped the URI, so it may not be pretty. Embed the
+ // escaped URL in the action URI since that URL should be "canonical". But
+ // pass the pretty, unescaped URL as the match comment, since it's likely
+ // to be displayed to the user, and in any case the front-end should not
+ // rely on it being canonical.
+ let escapedURL = uri.spec;
+ let displayURL = textURIService.unEscapeURIForUI("UTF-8", uri.spec);
+
+ let value = PlacesUtils.mozActionURI("visiturl", {
+ url: escapedURL,
+ input: this._originalSearchString,
+ });
+
+ let match = {
+ value: value,
+ comment: displayURL,
+ style: "action visiturl",
+ frecency: 0,
+ };
+
+ try {
+ let favicon = yield PlacesUtils.promiseFaviconLinkUrl(uri);
+ if (favicon)
+ match.icon = favicon.spec;
+ } catch (e) {
+ // It's possible we don't have a favicon for this - and that's ok.
+ }
+
+ this._addMatch(match);
+ return true;
+ },
+
+ _onResultRow: function (row) {
+ if (this._localMatchesCount == 0) {
+ TelemetryStopwatch.finish(TELEMETRY_1ST_RESULT, this);
+ }
+ let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE);
+ let match;
+ switch (queryType) {
+ case QUERYTYPE_AUTOFILL_HOST:
+ this._result.setDefaultIndex(0);
+ match = this._processHostRow(row);
+ break;
+ case QUERYTYPE_AUTOFILL_URL:
+ this._result.setDefaultIndex(0);
+ match = this._processUrlRow(row);
+ break;
+ case QUERYTYPE_FILTERED:
+ match = this._processRow(row);
+ break;
+ }
+ this._addMatch(match);
+ // If the search has been canceled by the user or by _addMatch, or we
+ // fetched enough results, we can stop the underlying Sqlite query.
+ if (!this.pending || this._localMatchesCount == Prefs.maxRichResults)
+ throw StopIteration;
+ },
+
+ _maybeRestyleSearchMatch: function (match) {
+ // Return if the URL does not represent a search result.
+ let parseResult =
+ PlacesSearchAutocompleteProvider.parseSubmissionURL(match.value);
+ if (!parseResult) {
+ return;
+ }
+
+ // Do not apply the special style if the user is doing a search from the
+ // location bar but the entered terms match an irrelevant portion of the
+ // URL. For example, "https://www.google.com/search?q=terms&client=firefox"
+ // when searching for "Firefox".
+ let terms = parseResult.terms.toLowerCase();
+ if (this._searchTokens.length > 0 &&
+ this._searchTokens.every(token => !terms.includes(token))) {
+ return;
+ }
+
+ // Turn the match into a searchengine action with a favicon.
+ match.value = PlacesUtils.mozActionURI("searchengine", {
+ engineName: parseResult.engineName,
+ input: parseResult.terms,
+ searchQuery: parseResult.terms,
+ });
+ match.comment = parseResult.engineName;
+ match.icon = match.icon || match.iconUrl;
+ match.style = "action searchengine favicon";
+ },
+
+ _addMatch(match) {
+ // 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;
+
+ // Must check both id and url, cause keywords dynamically modify the url.
+ let urlMapKey = makeKeyForURL(match.value);
+ if ((match.placeId && this._usedPlaceIds.has(match.placeId)) ||
+ this._usedURLs.has(urlMapKey)) {
+ return;
+ }
+
+ // 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(match.placeId);
+ this._usedURLs.add(urlMapKey);
+
+ match.style = match.style || "favicon";
+
+ // Restyle past searches, unless they are bookmarks or special results.
+ if (Prefs.restyleSearches && match.style == "favicon") {
+ this._maybeRestyleSearchMatch(match);
+ }
+
+ if (this._addingHeuristicFirstMatch) {
+ match.style += " heuristic";
+ }
+
+ match.icon = match.icon || "";
+ match.finalCompleteValue = match.finalCompleteValue || "";
+
+ this._result.insertMatchAt(this._getInsertIndexForMatch(match),
+ match.value,
+ match.comment,
+ match.icon,
+ match.style,
+ match.finalCompleteValue);
+
+ if (this._result.matchCount == 6)
+ TelemetryStopwatch.finish(TELEMETRY_6_FIRST_RESULTS, this);
+
+ this.notifyResults(true);
+ },
+
+ _getInsertIndexForMatch(match) {
+ let index = 0;
+ if (match.remote) {
+ // Append after local matches.
+ index = this._remoteMatchesStartIndex + this._remoteMatchesCount;
+ this._remoteMatchesCount++;
+ } else if (match.extension) {
+ index = this._localMatchesStartIndex;
+ this._localMatchesStartIndex++;
+ this._remoteMatchesStartIndex++;
+ this._extensionMatchesCount++;
+ } else {
+ // This is a local match.
+ if (match.frecency > FRECENCY_DEFAULT ||
+ this._localMatchesCount < MINIMUM_LOCAL_MATCHES) {
+ // Append before remote matches.
+ index = this._remoteMatchesStartIndex;
+ this._remoteMatchesStartIndex++
+ } else {
+ // Append after remote matches.
+ index = this._localMatchesCount + this._remoteMatchesCount;
+ }
+ this._localMatchesCount++;
+ }
+ return index;
+ },
+
+ _processHostRow: function (row) {
+ let match = {};
+ let trimmedHost = row.getResultByIndex(QUERYINDEX_URL);
+ let untrimmedHost = row.getResultByIndex(QUERYINDEX_TITLE);
+ let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
+ let faviconUrl = row.getResultByIndex(QUERYINDEX_ICONURL);
+
+ // If the untrimmed value doesn't preserve the user's input just
+ // ignore it and complete to the found host.
+ if (untrimmedHost &&
+ !untrimmedHost.toLowerCase().includes(this._trimmedOriginalSearchString.toLowerCase())) {
+ untrimmedHost = null;
+ }
+
+ match.value = this._strippedPrefix + trimmedHost;
+ // Remove the trailing slash.
+ match.comment = stripHttpAndTrim(trimmedHost);
+ match.finalCompleteValue = untrimmedHost;
+ if (faviconUrl) {
+ match.icon = PlacesUtils.favicons
+ .getFaviconLinkForIcon(NetUtil.newURI(faviconUrl)).spec;
+ }
+ // Although this has a frecency, this query is executed before any other
+ // queries that would result in frecency matches.
+ match.frecency = frecency;
+ match.style = "autofill";
+ return match;
+ },
+
+ _processUrlRow: function (row) {
+ let match = {};
+ let value = row.getResultByIndex(QUERYINDEX_URL);
+ let url = fixupSearchText(value);
+ let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
+ let faviconUrl = row.getResultByIndex(QUERYINDEX_ICONURL);
+
+ let prefix = value.slice(0, value.length - stripPrefix(value).length);
+
+ // We must complete the URL up to the next separator (which is /, ? or #).
+ let separatorIndex = url.slice(this._searchString.length)
+ .search(/[\/\?\#]/);
+ if (separatorIndex != -1) {
+ separatorIndex += this._searchString.length;
+ if (url[separatorIndex] == "/") {
+ separatorIndex++; // Include the "/" separator
+ }
+ url = url.slice(0, separatorIndex);
+ }
+
+ // If the untrimmed value doesn't preserve the user's input just
+ // ignore it and complete to the found url.
+ let untrimmedURL = prefix + url;
+ if (untrimmedURL &&
+ !untrimmedURL.toLowerCase().includes(this._trimmedOriginalSearchString.toLowerCase())) {
+ untrimmedURL = null;
+ }
+
+ match.value = this._strippedPrefix + url;
+ match.comment = url;
+ match.finalCompleteValue = untrimmedURL;
+ if (faviconUrl) {
+ match.icon = PlacesUtils.favicons
+ .getFaviconLinkForIcon(NetUtil.newURI(faviconUrl)).spec;
+ }
+ // Although this has a frecency, this query is executed before any other
+ // queries that would result in frecency matches.
+ match.frecency = frecency;
+ match.style = "autofill";
+ return match;
+ },
+
+ _processRow: function (row) {
+ let match = {};
+ match.placeId = row.getResultByIndex(QUERYINDEX_PLACEID);
+ let escapedURL = row.getResultByIndex(QUERYINDEX_URL);
+ let openPageCount = row.getResultByIndex(QUERYINDEX_SWITCHTAB) || 0;
+ let historyTitle = row.getResultByIndex(QUERYINDEX_TITLE) || "";
+ let iconurl = row.getResultByIndex(QUERYINDEX_ICONURL) || "";
+ let bookmarked = row.getResultByIndex(QUERYINDEX_BOOKMARKED);
+ let bookmarkTitle = bookmarked ?
+ row.getResultByIndex(QUERYINDEX_BOOKMARKTITLE) : null;
+ let tags = row.getResultByIndex(QUERYINDEX_TAGS) || "";
+ let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
+
+ // If actions are enabled and the page is open, add only the switch-to-tab
+ // result. Otherwise, add the normal result.
+ let url = escapedURL;
+ let action = null;
+ if (this._enableActions && openPageCount > 0 && this.hasBehavior("openpage")) {
+ url = PlacesUtils.mozActionURI("switchtab", {url: escapedURL});
+ action = "switchtab";
+ }
+
+ // Always prefer the bookmark title unless it is empty
+ let title = bookmarkTitle || historyTitle;
+
+ // We will always prefer to show tags if we have them.
+ let showTags = !!tags;
+
+ // However, we'll act as if a page is not bookmarked if the user wants
+ // only history and not bookmarks and there are no tags.
+ if (this.hasBehavior("history") && !this.hasBehavior("bookmark") &&
+ !showTags) {
+ showTags = false;
+ match.style = "favicon";
+ }
+
+ // If we have tags and should show them, we need to add them to the title.
+ if (showTags) {
+ title += TITLE_TAGS_SEPARATOR + tags;
+ }
+
+ // We have to determine the right style to display. Tags show the tag icon,
+ // bookmarks get the bookmark icon, and keywords get the keyword icon. If
+ // the result does not fall into any of those, it just gets the favicon.
+ if (!match.style) {
+ // It is possible that we already have a style set (from a keyword
+ // search or because of the user's preferences), so only set it if we
+ // haven't already done so.
+ if (showTags) {
+ // 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";
+ }
+ }
+
+ if (action)
+ match.style = "action " + action;
+
+ match.value = url;
+ match.comment = title;
+ if (iconurl) {
+ match.icon = PlacesUtils.favicons
+ .getFaviconLinkForIcon(NetUtil.newURI(iconurl)).spec;
+ }
+ match.frecency = frecency;
+
+ return match;
+ },
+
+ /**
+ * @return a string consisting of the search query to be used based on the
+ * previously set urlbar suggestion preferences.
+ */
+ get _suggestionPrefQuery() {
+ if (!this.hasBehavior("restrict") && this.hasBehavior("history") &&
+ this.hasBehavior("bookmark")) {
+ return this.hasBehavior("typed") ? defaultQuery("AND h.typed = 1")
+ : defaultQuery();
+ }
+ let conditions = [];
+ 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("typed")) {
+ conditions.push("h.typed = 1");
+ }
+ if (this.hasBehavior("bookmark")) {
+ conditions.push("bookmarked");
+ }
+ if (this.hasBehavior("tag")) {
+ conditions.push("tags NOTNULL");
+ }
+
+ return conditions.length ? defaultQuery("AND " + conditions.join(" AND "))
+ : defaultQuery();
+ },
+
+ /**
+ * Obtains the search query to be used based on the previously set search
+ * preferences (accessed by this.hasBehavior).
+ *
+ * @return an array consisting of the correctly optimized query to search the
+ * database with and an object containing the params to bound.
+ */
+ get _searchQuery() {
+ let query = this._suggestionPrefQuery;
+
+ return [
+ query,
+ {
+ parent: PlacesUtils.tagsFolderId,
+ query_type: QUERYTYPE_FILTERED,
+ 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._searchTokens.join(" "),
+ userContextId: this._userContextId,
+ // Limit the query to the the maximum number of desired results.
+ // This way we can avoid doing more work than needed.
+ maxResults: Prefs.maxRichResults
+ }
+ ];
+ },
+
+ /**
+ * Obtains the query to search for switch-to-tab entries.
+ *
+ * @return 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,
+ {
+ query_type: QUERYTYPE_FILTERED,
+ 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._searchTokens.join(" "),
+ userContextId: this._userContextId,
+ maxResults: Prefs.maxRichResults
+ }
+ ];
+ },
+
+ /**
+ * Obtains the query to search for adaptive results.
+ *
+ * @return an array consisting of the correctly optimized query to search the
+ * database with and an object containing the params to bound.
+ */
+ get _adaptiveQuery() {
+ return [
+ SQL_ADAPTIVE_QUERY,
+ {
+ parent: PlacesUtils.tagsFolderId,
+ search_string: this._searchString,
+ query_type: QUERYTYPE_FILTERED,
+ matchBehavior: this._matchBehavior,
+ searchBehavior: this._behavior,
+ userContextId: this._userContextId,
+ }
+ ];
+ },
+
+ /**
+ * Whether we should try to autoFill.
+ */
+ get _shouldAutofill() {
+ // First of all, check for the autoFill pref.
+ if (!Prefs.autofill)
+ return false;
+
+ if (this._searchTokens.length != 1)
+ return false;
+
+ // autoFill can only cope with history or bookmarks entries.
+ if (!this.hasBehavior("history") &&
+ !this.hasBehavior("bookmark"))
+ return false;
+
+ // autoFill doesn't search titles or tags.
+ if (this.hasBehavior("title") || this.hasBehavior("tag"))
+ return false;
+
+ // Don't try to autofill if the search term includes any whitespace.
+ // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
+ // tokenizer ends up trimming the search string and returning a value
+ // that doesn't match it, or is even shorter.
+ if (REGEXP_SPACES.test(this._originalSearchString))
+ return false;
+
+ if (this._searchString.length == 0)
+ return false;
+
+ if (this._prohibitAutoFill)
+ return false;
+
+ return true;
+ },
+
+ /**
+ * Obtains the query to search for autoFill host results.
+ *
+ * @return an array consisting of the correctly optimized query to search the
+ * database with and an object containing the params to bound.
+ */
+ get _hostQuery() {
+ let typed = Prefs.autofillTyped || this.hasBehavior("typed");
+ let bookmarked = this.hasBehavior("bookmark") && !this.hasBehavior("history");
+
+ let query = [];
+ if (bookmarked) {
+ query.push(typed ? SQL_BOOKMARKED_TYPED_HOST_QUERY
+ : SQL_BOOKMARKED_HOST_QUERY);
+ } else {
+ query.push(typed ? SQL_TYPED_HOST_QUERY
+ : SQL_HOST_QUERY);
+ }
+
+ query.push({
+ query_type: QUERYTYPE_AUTOFILL_HOST,
+ searchString: this._searchString.toLowerCase()
+ });
+
+ return query;
+ },
+
+ /**
+ * Obtains the query to search for autoFill url results.
+ *
+ * @return an array consisting of the correctly optimized query to search the
+ * database with and an object containing the params to bound.
+ */
+ get _urlQuery() {
+ // We expect this to be a full URL, not just a host. We want to extract the
+ // host and use that as a guess for whether we'll get a result from a URL
+ // query.
+ let slashIndex = this._autofillUrlSearchString.indexOf("/");
+ let revHost = this._autofillUrlSearchString.substring(0, slashIndex).toLowerCase()
+ .split("").reverse().join("") + ".";
+
+ let typed = Prefs.autofillTyped || this.hasBehavior("typed");
+ let bookmarked = this.hasBehavior("bookmark") && !this.hasBehavior("history");
+
+ let query = [];
+ if (bookmarked) {
+ query.push(typed ? SQL_BOOKMARKED_TYPED_URL_QUERY
+ : SQL_BOOKMARKED_URL_QUERY);
+ } else {
+ query.push(typed ? SQL_TYPED_URL_QUERY
+ : SQL_URL_QUERY);
+ }
+
+ query.push({
+ query_type: QUERYTYPE_AUTOFILL_URL,
+ searchString: this._autofillUrlSearchString,
+ revHost
+ });
+
+ return query;
+ },
+
+ /**
+ * Notifies the listener about results.
+ *
+ * @param searchOngoing
+ * Indicates whether the search is ongoing.
+ */
+ notifyResults: function (searchOngoing) {
+ let result = this._result;
+ let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
+ if (searchOngoing) {
+ resultCode += "_ONGOING";
+ }
+ result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
+ this._listener.onSearchResult(this._autocompleteSearch, result);
+ },
+}
+
+// UnifiedComplete class
+// component @mozilla.org/autocomplete/search;1?name=unifiedcomplete
+
+function UnifiedComplete() {
+ // Make sure the preferences are initialized as soon as possible.
+ // If the value of browser.urlbar.autocomplete.enabled is set to false,
+ // then all the other suggest preferences for history, bookmarks and
+ // open pages should be set to false.
+ Prefs;
+}
+
+UnifiedComplete.prototype = {
+ // Database handling
+
+ /**
+ * Promise resolved when the database initialization has completed, or null
+ * if it has never been requested.
+ */
+ _promiseDatabase: null,
+
+ /**
+ * Gets a Sqlite database handle.
+ *
+ * @return {Promise}
+ * @resolves to the Sqlite database handle (according to Sqlite.jsm).
+ * @rejects javascript exception.
+ */
+ getDatabaseHandle: function () {
+ if (Prefs.enabled && !this._promiseDatabase) {
+ this._promiseDatabase = Task.spawn(function* () {
+ let conn = yield Sqlite.cloneStorageConnection({
+ connection: PlacesUtils.history.DBConnection,
+ readOnly: true
+ });
+
+ try {
+ Sqlite.shutdown.addBlocker("Places UnifiedComplete.js clone closing",
+ Task.async(function* () {
+ SwitchToTabStorage.shutdown();
+ yield conn.close();
+ }));
+ } catch (ex) {
+ // It's too late to block shutdown, just close the connection.
+ yield conn.close();
+ throw ex;
+ }
+
+ // Autocomplete often fallbacks to a table scan due to lack of text
+ // indices. A larger cache helps reducing IO and improving performance.
+ // The value used here is larger than the default Storage value defined
+ // as MAX_CACHE_SIZE_BYTES in storage/mozStorageConnection.cpp.
+ yield conn.execute("PRAGMA cache_size = -6144"); // 6MiB
+
+ yield SwitchToTabStorage.initDatabase(conn);
+
+ return conn;
+ }.bind(this)).then(null, ex => { dump("Couldn't get database handle: " + ex + "\n");
+ Cu.reportError(ex); });
+ }
+ return this._promiseDatabase;
+ },
+
+ // mozIPlacesAutoComplete
+
+ registerOpenPage(uri, userContextId) {
+ SwitchToTabStorage.add(uri, userContextId);
+ },
+
+ unregisterOpenPage(uri, userContextId) {
+ SwitchToTabStorage.delete(uri, userContextId);
+ },
+
+ // nsIAutoCompleteSearch
+
+ startSearch: function (searchString, searchParam, previousResult, listener) {
+ // Stop the search in case the controller has not taken care of it.
+ if (this._currentSearch) {
+ this.stopSearch();
+ }
+
+ // Note: We don't use previousResult to make sure ordering of results are
+ // consistent. See bug 412730 for more details.
+
+ // If the previous search didn't fetch enough search suggestions, it's
+ // unlikely a longer text would do.
+ let prohibitSearchSuggestions =
+ this._lastLowResultsSearchSuggestion &&
+ searchString.length > this._lastLowResultsSearchSuggestion.length &&
+ searchString.startsWith(this._lastLowResultsSearchSuggestion);
+
+ this._currentSearch = new Search(searchString, searchParam, listener,
+ this, this, prohibitSearchSuggestions);
+
+ // If we are not enabled, we need to return now. Notice we need an empty
+ // result regardless, so we still create the Search object.
+ if (!Prefs.enabled) {
+ this.finishSearch(true);
+ return;
+ }
+
+ let search = this._currentSearch;
+ this.getDatabaseHandle().then(conn => search.execute(conn))
+ .then(null, ex => {
+ dump(`Query failed: ${ex}\n`);
+ Cu.reportError(ex);
+ })
+ .then(() => {
+ if (search == this._currentSearch) {
+ this.finishSearch(true);
+ }
+ });
+ },
+
+ stopSearch: function () {
+ if (this._currentSearch) {
+ this._currentSearch.stop();
+ }
+ // 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 notify [optional]
+ * Indicates if we should notify the AutoComplete listener about our
+ * results or not.
+ */
+ finishSearch: function (notify=false) {
+ TelemetryStopwatch.cancel(TELEMETRY_1ST_RESULT, this);
+ TelemetryStopwatch.cancel(TELEMETRY_6_FIRST_RESULTS, this);
+ // Clear state now to avoid race conditions, see below.
+ let search = this._currentSearch;
+ if (!search)
+ return;
+ this._lastLowResultsSearchSuggestion = search._lastLowResultsSearchSuggestion;
+ delete this._currentSearch;
+
+ if (!notify)
+ 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
+ // notifyResults.
+ // Thus, ensure that notifyResults is the last call in this method,
+ // otherwise you might be touching the wrong search.
+ search.notifyResults(false);
+ },
+
+ // nsIAutoCompleteSimpleResultListener
+
+ onValueRemoved: function (result, spec, removeFromDB) {
+ if (removeFromDB) {
+ PlacesUtils.history.removePage(NetUtil.newURI(spec));
+ }
+ },
+
+ // nsIAutoCompleteSearchDescriptor
+
+ get searchType() {
+ return Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE;
+ },
+
+ get clearingAutoFillSearchesAgain() {
+ return true;
+ },
+
+ // nsISupports
+
+ classID: Components.ID("f964a319-397a-4d21-8be6-5cdd1ee3e3ae"),
+
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(UnifiedComplete),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAutoCompleteSearch,
+ Ci.nsIAutoCompleteSimpleResultListener,
+ Ci.nsIAutoCompleteSearchDescriptor,
+ Ci.mozIPlacesAutoComplete,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference
+ ])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UnifiedComplete]);