Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
michael@0 | 1 | /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- |
michael@0 | 2 | * vim: sw=2 ts=2 sts=2 expandtab |
michael@0 | 3 | * This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 6 | |
michael@0 | 7 | "use strict"; |
michael@0 | 8 | |
michael@0 | 9 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 10 | //// Constants |
michael@0 | 11 | |
michael@0 | 12 | const Cc = Components.classes; |
michael@0 | 13 | const Ci = Components.interfaces; |
michael@0 | 14 | const Cr = Components.results; |
michael@0 | 15 | const Cu = Components.utils; |
michael@0 | 16 | |
michael@0 | 17 | const TOPIC_SHUTDOWN = "places-shutdown"; |
michael@0 | 18 | const TOPIC_PREFCHANGED = "nsPref:changed"; |
michael@0 | 19 | |
michael@0 | 20 | const DEFAULT_BEHAVIOR = 0; |
michael@0 | 21 | |
michael@0 | 22 | const PREF_BRANCH = "browser.urlbar"; |
michael@0 | 23 | |
michael@0 | 24 | // Prefs are defined as [pref name, default value]. |
michael@0 | 25 | const PREF_ENABLED = [ "autocomplete.enabled", true ]; |
michael@0 | 26 | const PREF_AUTOFILL = [ "autoFill", true ]; |
michael@0 | 27 | const PREF_AUTOFILL_TYPED = [ "autoFill.typed", true ]; |
michael@0 | 28 | const PREF_AUTOFILL_PRIORITY = [ "autoFill.priority", true ]; |
michael@0 | 29 | const PREF_DELAY = [ "delay", 50 ]; |
michael@0 | 30 | const PREF_BEHAVIOR = [ "matchBehavior", MATCH_BOUNDARY_ANYWHERE ]; |
michael@0 | 31 | const PREF_DEFAULT_BEHAVIOR = [ "default.behavior", DEFAULT_BEHAVIOR ]; |
michael@0 | 32 | const PREF_EMPTY_BEHAVIOR = [ "default.behavior.emptyRestriction", |
michael@0 | 33 | Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY | |
michael@0 | 34 | Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED ]; |
michael@0 | 35 | const PREF_FILTER_JS = [ "filter.javascript", true ]; |
michael@0 | 36 | const PREF_MAXRESULTS = [ "maxRichResults", 25 ]; |
michael@0 | 37 | const PREF_RESTRICT_HISTORY = [ "restrict.history", "^" ]; |
michael@0 | 38 | const PREF_RESTRICT_BOOKMARKS = [ "restrict.bookmark", "*" ]; |
michael@0 | 39 | const PREF_RESTRICT_TYPED = [ "restrict.typed", "~" ]; |
michael@0 | 40 | const PREF_RESTRICT_TAG = [ "restrict.tag", "+" ]; |
michael@0 | 41 | const PREF_RESTRICT_SWITCHTAB = [ "restrict.openpage", "%" ]; |
michael@0 | 42 | const PREF_MATCH_TITLE = [ "match.title", "#" ]; |
michael@0 | 43 | const PREF_MATCH_URL = [ "match.url", "@" ]; |
michael@0 | 44 | |
michael@0 | 45 | // Match type constants. |
michael@0 | 46 | // These indicate what type of search function we should be using. |
michael@0 | 47 | const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE; |
michael@0 | 48 | const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE; |
michael@0 | 49 | const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY; |
michael@0 | 50 | const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING; |
michael@0 | 51 | const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE; |
michael@0 | 52 | |
michael@0 | 53 | // AutoComplete query type constants. |
michael@0 | 54 | // Describes the various types of queries that we can process rows for. |
michael@0 | 55 | const QUERYTYPE_KEYWORD = 0; |
michael@0 | 56 | const QUERYTYPE_FILTERED = 1; |
michael@0 | 57 | const QUERYTYPE_AUTOFILL_HOST = 2; |
michael@0 | 58 | const QUERYTYPE_AUTOFILL_URL = 3; |
michael@0 | 59 | |
michael@0 | 60 | // This separator is used as an RTL-friendly way to split the title and tags. |
michael@0 | 61 | // It can also be used by an nsIAutoCompleteResult consumer to re-split the |
michael@0 | 62 | // "comment" back into the title and the tag. |
michael@0 | 63 | const TITLE_TAGS_SEPARATOR = " \u2013 "; |
michael@0 | 64 | |
michael@0 | 65 | // Telemetry probes. |
michael@0 | 66 | const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS"; |
michael@0 | 67 | |
michael@0 | 68 | // The default frecency value used when inserting priority results. |
michael@0 | 69 | const FRECENCY_PRIORITY_DEFAULT = 1000; |
michael@0 | 70 | |
michael@0 | 71 | // Sqlite result row index constants. |
michael@0 | 72 | const QUERYINDEX_QUERYTYPE = 0; |
michael@0 | 73 | const QUERYINDEX_URL = 1; |
michael@0 | 74 | const QUERYINDEX_TITLE = 2; |
michael@0 | 75 | const QUERYINDEX_ICONURL = 3; |
michael@0 | 76 | const QUERYINDEX_BOOKMARKED = 4; |
michael@0 | 77 | const QUERYINDEX_BOOKMARKTITLE = 5; |
michael@0 | 78 | const QUERYINDEX_TAGS = 6; |
michael@0 | 79 | const QUERYINDEX_VISITCOUNT = 7; |
michael@0 | 80 | const QUERYINDEX_TYPED = 8; |
michael@0 | 81 | const QUERYINDEX_PLACEID = 9; |
michael@0 | 82 | const QUERYINDEX_SWITCHTAB = 10; |
michael@0 | 83 | const QUERYINDEX_FRECENCY = 11; |
michael@0 | 84 | |
michael@0 | 85 | // This SQL query fragment provides the following: |
michael@0 | 86 | // - whether the entry is bookmarked (QUERYINDEX_BOOKMARKED) |
michael@0 | 87 | // - the bookmark title, if it is a bookmark (QUERYINDEX_BOOKMARKTITLE) |
michael@0 | 88 | // - the tags associated with a bookmarked entry (QUERYINDEX_TAGS) |
michael@0 | 89 | const SQL_BOOKMARK_TAGS_FRAGMENT = sql( |
michael@0 | 90 | "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked,", |
michael@0 | 91 | "( SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL", |
michael@0 | 92 | "ORDER BY lastModified DESC LIMIT 1", |
michael@0 | 93 | ") AS btitle,", |
michael@0 | 94 | "( SELECT GROUP_CONCAT(t.title, ',')", |
michael@0 | 95 | "FROM moz_bookmarks b", |
michael@0 | 96 | "JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent", |
michael@0 | 97 | "WHERE b.fk = h.id", |
michael@0 | 98 | ") AS tags"); |
michael@0 | 99 | |
michael@0 | 100 | // TODO bug 412736: in case of a frecency tie, we might break it with h.typed |
michael@0 | 101 | // and h.visit_count. That is slower though, so not doing it yet... |
michael@0 | 102 | const SQL_DEFAULT_QUERY = sql( |
michael@0 | 103 | "SELECT :query_type, h.url, h.title, f.url,", SQL_BOOKMARK_TAGS_FRAGMENT, ",", |
michael@0 | 104 | "h.visit_count, h.typed, h.id, t.open_count, h.frecency", |
michael@0 | 105 | "FROM moz_places h", |
michael@0 | 106 | "LEFT JOIN moz_favicons f ON f.id = h.favicon_id", |
michael@0 | 107 | "LEFT JOIN moz_openpages_temp t ON t.url = h.url", |
michael@0 | 108 | "WHERE h.frecency <> 0", |
michael@0 | 109 | "AND AUTOCOMPLETE_MATCH(:searchString, h.url,", |
michael@0 | 110 | "IFNULL(btitle, h.title), tags,", |
michael@0 | 111 | "h.visit_count, h.typed,", |
michael@0 | 112 | "bookmarked, t.open_count,", |
michael@0 | 113 | ":matchBehavior, :searchBehavior)", |
michael@0 | 114 | "/*CONDITIONS*/", |
michael@0 | 115 | "ORDER BY h.frecency DESC, h.id DESC", |
michael@0 | 116 | "LIMIT :maxResults"); |
michael@0 | 117 | |
michael@0 | 118 | // Enforce ignoring the visit_count index, since the frecency one is much |
michael@0 | 119 | // faster in this case. ANALYZE helps the query planner to figure out the |
michael@0 | 120 | // faster path, but it may not have up-to-date information yet. |
michael@0 | 121 | const SQL_HISTORY_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/", |
michael@0 | 122 | "AND +h.visit_count > 0", "g"); |
michael@0 | 123 | |
michael@0 | 124 | const SQL_BOOKMARK_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/", |
michael@0 | 125 | "AND bookmarked", "g"); |
michael@0 | 126 | |
michael@0 | 127 | const SQL_TAGS_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/", |
michael@0 | 128 | "AND tags NOTNULL", "g"); |
michael@0 | 129 | |
michael@0 | 130 | const SQL_TYPED_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/", |
michael@0 | 131 | "AND h.typed = 1", "g"); |
michael@0 | 132 | |
michael@0 | 133 | const SQL_SWITCHTAB_QUERY = sql( |
michael@0 | 134 | "SELECT :query_type, t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL,", |
michael@0 | 135 | "t.open_count, NULL", |
michael@0 | 136 | "FROM moz_openpages_temp t", |
michael@0 | 137 | "LEFT JOIN moz_places h ON h.url = t.url", |
michael@0 | 138 | "WHERE h.id IS NULL", |
michael@0 | 139 | "AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL,", |
michael@0 | 140 | "NULL, NULL, NULL, t.open_count,", |
michael@0 | 141 | ":matchBehavior, :searchBehavior)", |
michael@0 | 142 | "ORDER BY t.ROWID DESC", |
michael@0 | 143 | "LIMIT :maxResults"); |
michael@0 | 144 | |
michael@0 | 145 | const SQL_ADAPTIVE_QUERY = sql( |
michael@0 | 146 | "/* do not warn (bug 487789) */", |
michael@0 | 147 | "SELECT :query_type, h.url, h.title, f.url,", SQL_BOOKMARK_TAGS_FRAGMENT, ",", |
michael@0 | 148 | "h.visit_count, h.typed, h.id, t.open_count, h.frecency", |
michael@0 | 149 | "FROM (", |
michael@0 | 150 | "SELECT ROUND(MAX(use_count) * (1 + (input = :search_string)), 1) AS rank,", |
michael@0 | 151 | "place_id", |
michael@0 | 152 | "FROM moz_inputhistory", |
michael@0 | 153 | "WHERE input BETWEEN :search_string AND :search_string || X'FFFF'", |
michael@0 | 154 | "GROUP BY place_id", |
michael@0 | 155 | ") AS i", |
michael@0 | 156 | "JOIN moz_places h ON h.id = i.place_id", |
michael@0 | 157 | "LEFT JOIN moz_favicons f ON f.id = h.favicon_id", |
michael@0 | 158 | "LEFT JOIN moz_openpages_temp t ON t.url = h.url", |
michael@0 | 159 | "WHERE AUTOCOMPLETE_MATCH(NULL, h.url,", |
michael@0 | 160 | "IFNULL(btitle, h.title), tags,", |
michael@0 | 161 | "h.visit_count, h.typed, bookmarked,", |
michael@0 | 162 | "t.open_count,", |
michael@0 | 163 | ":matchBehavior, :searchBehavior)", |
michael@0 | 164 | "ORDER BY rank DESC, h.frecency DESC"); |
michael@0 | 165 | |
michael@0 | 166 | const SQL_KEYWORD_QUERY = sql( |
michael@0 | 167 | "/* do not warn (bug 487787) */", |
michael@0 | 168 | "SELECT :query_type,", |
michael@0 | 169 | "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk)", |
michael@0 | 170 | "AS search_url, h.title,", |
michael@0 | 171 | "IFNULL(f.url, (SELECT f.url", |
michael@0 | 172 | "FROM moz_places", |
michael@0 | 173 | "JOIN moz_favicons f ON f.id = favicon_id", |
michael@0 | 174 | "WHERE rev_host = (SELECT rev_host FROM moz_places WHERE id = b.fk)", |
michael@0 | 175 | "ORDER BY frecency DESC", |
michael@0 | 176 | "LIMIT 1)", |
michael@0 | 177 | "),", |
michael@0 | 178 | "1, b.title, NULL, h.visit_count, h.typed, IFNULL(h.id, b.fk),", |
michael@0 | 179 | "t.open_count, h.frecency", |
michael@0 | 180 | "FROM moz_keywords k", |
michael@0 | 181 | "JOIN moz_bookmarks b ON b.keyword_id = k.id", |
michael@0 | 182 | "LEFT JOIN moz_places h ON h.url = search_url", |
michael@0 | 183 | "LEFT JOIN moz_favicons f ON f.id = h.favicon_id", |
michael@0 | 184 | "LEFT JOIN moz_openpages_temp t ON t.url = search_url", |
michael@0 | 185 | "WHERE LOWER(k.keyword) = LOWER(:keyword)", |
michael@0 | 186 | "ORDER BY h.frecency DESC"); |
michael@0 | 187 | |
michael@0 | 188 | const SQL_HOST_QUERY = sql( |
michael@0 | 189 | "/* do not warn (bug NA): not worth to index on (typed, frecency) */", |
michael@0 | 190 | "SELECT :query_type, host || '/', prefix || host || '/',", |
michael@0 | 191 | "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, frecency", |
michael@0 | 192 | "FROM moz_hosts", |
michael@0 | 193 | "WHERE host BETWEEN :searchString AND :searchString || X'FFFF'", |
michael@0 | 194 | "AND frecency <> 0", |
michael@0 | 195 | "/*CONDITIONS*/", |
michael@0 | 196 | "ORDER BY frecency DESC", |
michael@0 | 197 | "LIMIT 1"); |
michael@0 | 198 | |
michael@0 | 199 | const SQL_TYPED_HOST_QUERY = SQL_HOST_QUERY.replace("/*CONDITIONS*/", |
michael@0 | 200 | "AND typed = 1"); |
michael@0 | 201 | const SQL_URL_QUERY = sql( |
michael@0 | 202 | "/* do not warn (bug no): cannot use an index */", |
michael@0 | 203 | "SELECT :query_type, h.url,", |
michael@0 | 204 | "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, h.frecency", |
michael@0 | 205 | "FROM moz_places h", |
michael@0 | 206 | "WHERE h.frecency <> 0", |
michael@0 | 207 | "/*CONDITIONS*/", |
michael@0 | 208 | "AND AUTOCOMPLETE_MATCH(:searchString, h.url,", |
michael@0 | 209 | "h.title, '',", |
michael@0 | 210 | "h.visit_count, h.typed, 0, 0,", |
michael@0 | 211 | ":matchBehavior, :searchBehavior)", |
michael@0 | 212 | "ORDER BY h.frecency DESC, h.id DESC", |
michael@0 | 213 | "LIMIT 1"); |
michael@0 | 214 | |
michael@0 | 215 | const SQL_TYPED_URL_QUERY = SQL_URL_QUERY.replace("/*CONDITIONS*/", |
michael@0 | 216 | "AND typed = 1"); |
michael@0 | 217 | |
michael@0 | 218 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 219 | //// Getters |
michael@0 | 220 | |
michael@0 | 221 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 222 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 223 | |
michael@0 | 224 | XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", |
michael@0 | 225 | "resource://gre/modules/PlacesUtils.jsm"); |
michael@0 | 226 | XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", |
michael@0 | 227 | "resource://gre/modules/TelemetryStopwatch.jsm"); |
michael@0 | 228 | XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
michael@0 | 229 | "resource://gre/modules/NetUtil.jsm"); |
michael@0 | 230 | XPCOMUtils.defineLazyModuleGetter(this, "Preferences", |
michael@0 | 231 | "resource://gre/modules/Preferences.jsm"); |
michael@0 | 232 | XPCOMUtils.defineLazyModuleGetter(this, "Sqlite", |
michael@0 | 233 | "resource://gre/modules/Sqlite.jsm"); |
michael@0 | 234 | XPCOMUtils.defineLazyModuleGetter(this, "OS", |
michael@0 | 235 | "resource://gre/modules/osfile.jsm"); |
michael@0 | 236 | XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
michael@0 | 237 | "resource://gre/modules/Promise.jsm"); |
michael@0 | 238 | XPCOMUtils.defineLazyModuleGetter(this, "Task", |
michael@0 | 239 | "resource://gre/modules/Task.jsm"); |
michael@0 | 240 | XPCOMUtils.defineLazyModuleGetter(this, "PriorityUrlProvider", |
michael@0 | 241 | "resource://gre/modules/PriorityUrlProvider.jsm"); |
michael@0 | 242 | |
michael@0 | 243 | XPCOMUtils.defineLazyServiceGetter(this, "textURIService", |
michael@0 | 244 | "@mozilla.org/intl/texttosuburi;1", |
michael@0 | 245 | "nsITextToSubURI"); |
michael@0 | 246 | |
michael@0 | 247 | /** |
michael@0 | 248 | * Storage object for switch-to-tab entries. |
michael@0 | 249 | * This takes care of caching and registering open pages, that will be reused |
michael@0 | 250 | * by switch-to-tab queries. It has an internal cache, so that the Sqlite |
michael@0 | 251 | * store is lazy initialized only on first use. |
michael@0 | 252 | * It has a simple API: |
michael@0 | 253 | * initDatabase(conn): initializes the temporary Sqlite entities to store data |
michael@0 | 254 | * add(uri): adds a given nsIURI to the store |
michael@0 | 255 | * delete(uri): removes a given nsIURI from the store |
michael@0 | 256 | * shutdown(): stops storing data to Sqlite |
michael@0 | 257 | */ |
michael@0 | 258 | XPCOMUtils.defineLazyGetter(this, "SwitchToTabStorage", () => Object.seal({ |
michael@0 | 259 | _conn: null, |
michael@0 | 260 | // Temporary queue used while the database connection is not available. |
michael@0 | 261 | _queue: new Set(), |
michael@0 | 262 | initDatabase: Task.async(function* (conn) { |
michael@0 | 263 | // To reduce IO use an in-memory table for switch-to-tab tracking. |
michael@0 | 264 | // Note: this should be kept up-to-date with the definition in |
michael@0 | 265 | // nsPlacesTables.h. |
michael@0 | 266 | yield conn.execute(sql( |
michael@0 | 267 | "CREATE TEMP TABLE moz_openpages_temp (", |
michael@0 | 268 | "url TEXT PRIMARY KEY,", |
michael@0 | 269 | "open_count INTEGER", |
michael@0 | 270 | ")")); |
michael@0 | 271 | |
michael@0 | 272 | // Note: this should be kept up-to-date with the definition in |
michael@0 | 273 | // nsPlacesTriggers.h. |
michael@0 | 274 | yield conn.execute(sql( |
michael@0 | 275 | "CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger", |
michael@0 | 276 | "AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW", |
michael@0 | 277 | "WHEN NEW.open_count = 0", |
michael@0 | 278 | "BEGIN", |
michael@0 | 279 | "DELETE FROM moz_openpages_temp", |
michael@0 | 280 | "WHERE url = NEW.url;", |
michael@0 | 281 | "END")); |
michael@0 | 282 | |
michael@0 | 283 | this._conn = conn; |
michael@0 | 284 | |
michael@0 | 285 | // Populate the table with the current cache contents... |
michael@0 | 286 | this._queue.forEach(this.add, this); |
michael@0 | 287 | // ...then clear it to avoid double additions. |
michael@0 | 288 | this._queue.clear(); |
michael@0 | 289 | }), |
michael@0 | 290 | |
michael@0 | 291 | add: function (uri) { |
michael@0 | 292 | if (!this._conn) { |
michael@0 | 293 | this._queue.add(uri); |
michael@0 | 294 | return; |
michael@0 | 295 | } |
michael@0 | 296 | this._conn.executeCached(sql( |
michael@0 | 297 | "INSERT OR REPLACE INTO moz_openpages_temp (url, open_count)", |
michael@0 | 298 | "VALUES ( :url, IFNULL( (SELECT open_count + 1", |
michael@0 | 299 | "FROM moz_openpages_temp", |
michael@0 | 300 | "WHERE url = :url),", |
michael@0 | 301 | "1", |
michael@0 | 302 | ")", |
michael@0 | 303 | ")" |
michael@0 | 304 | ), { url: uri.spec }); |
michael@0 | 305 | }, |
michael@0 | 306 | |
michael@0 | 307 | delete: function (uri) { |
michael@0 | 308 | if (!this._conn) { |
michael@0 | 309 | this._queue.delete(uri); |
michael@0 | 310 | return; |
michael@0 | 311 | } |
michael@0 | 312 | this._conn.executeCached(sql( |
michael@0 | 313 | "UPDATE moz_openpages_temp", |
michael@0 | 314 | "SET open_count = open_count - 1", |
michael@0 | 315 | "WHERE url = :url" |
michael@0 | 316 | ), { url: uri.spec }); |
michael@0 | 317 | }, |
michael@0 | 318 | |
michael@0 | 319 | shutdown: function () { |
michael@0 | 320 | this._conn = null; |
michael@0 | 321 | this._queue.clear(); |
michael@0 | 322 | } |
michael@0 | 323 | })); |
michael@0 | 324 | |
michael@0 | 325 | /** |
michael@0 | 326 | * This helper keeps track of preferences and keeps their values up-to-date. |
michael@0 | 327 | */ |
michael@0 | 328 | XPCOMUtils.defineLazyGetter(this, "Prefs", () => { |
michael@0 | 329 | let prefs = new Preferences(PREF_BRANCH); |
michael@0 | 330 | |
michael@0 | 331 | function loadPrefs() { |
michael@0 | 332 | store.enabled = prefs.get(...PREF_ENABLED); |
michael@0 | 333 | store.autofill = prefs.get(...PREF_AUTOFILL); |
michael@0 | 334 | store.autofillTyped = prefs.get(...PREF_AUTOFILL_TYPED); |
michael@0 | 335 | store.autofillPriority = prefs.get(...PREF_AUTOFILL_PRIORITY); |
michael@0 | 336 | store.delay = prefs.get(...PREF_DELAY); |
michael@0 | 337 | store.matchBehavior = prefs.get(...PREF_BEHAVIOR); |
michael@0 | 338 | store.filterJavaScript = prefs.get(...PREF_FILTER_JS); |
michael@0 | 339 | store.maxRichResults = prefs.get(...PREF_MAXRESULTS); |
michael@0 | 340 | store.restrictHistoryToken = prefs.get(...PREF_RESTRICT_HISTORY); |
michael@0 | 341 | store.restrictBookmarkToken = prefs.get(...PREF_RESTRICT_BOOKMARKS); |
michael@0 | 342 | store.restrictTypedToken = prefs.get(...PREF_RESTRICT_TYPED); |
michael@0 | 343 | store.restrictTagToken = prefs.get(...PREF_RESTRICT_TAG); |
michael@0 | 344 | store.restrictOpenPageToken = prefs.get(...PREF_RESTRICT_SWITCHTAB); |
michael@0 | 345 | store.matchTitleToken = prefs.get(...PREF_MATCH_TITLE); |
michael@0 | 346 | store.matchURLToken = prefs.get(...PREF_MATCH_URL); |
michael@0 | 347 | store.defaultBehavior = prefs.get(...PREF_DEFAULT_BEHAVIOR); |
michael@0 | 348 | // Further restrictions to apply for "empty searches" (i.e. searches for ""). |
michael@0 | 349 | store.emptySearchDefaultBehavior = store.defaultBehavior | |
michael@0 | 350 | prefs.get(...PREF_EMPTY_BEHAVIOR); |
michael@0 | 351 | |
michael@0 | 352 | // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE. |
michael@0 | 353 | if (store.matchBehavior != MATCH_ANYWHERE && |
michael@0 | 354 | store.matchBehavior != MATCH_BOUNDARY && |
michael@0 | 355 | store.matchBehavior != MATCH_BEGINNING) { |
michael@0 | 356 | store.matchBehavior = MATCH_BOUNDARY_ANYWHERE; |
michael@0 | 357 | } |
michael@0 | 358 | |
michael@0 | 359 | store.tokenToBehaviorMap = new Map([ |
michael@0 | 360 | [ store.restrictHistoryToken, "history" ], |
michael@0 | 361 | [ store.restrictBookmarkToken, "bookmark" ], |
michael@0 | 362 | [ store.restrictTagToken, "tag" ], |
michael@0 | 363 | [ store.restrictOpenPageToken, "openpage" ], |
michael@0 | 364 | [ store.matchTitleToken, "title" ], |
michael@0 | 365 | [ store.matchURLToken, "url" ], |
michael@0 | 366 | [ store.restrictTypedToken, "typed" ] |
michael@0 | 367 | ]); |
michael@0 | 368 | } |
michael@0 | 369 | |
michael@0 | 370 | let store = { |
michael@0 | 371 | observe: function (subject, topic, data) { |
michael@0 | 372 | loadPrefs(); |
michael@0 | 373 | }, |
michael@0 | 374 | QueryInterface: XPCOMUtils.generateQI([ Ci.nsIObserver ]) |
michael@0 | 375 | }; |
michael@0 | 376 | loadPrefs(); |
michael@0 | 377 | prefs.observe("", store); |
michael@0 | 378 | |
michael@0 | 379 | return Object.seal(store); |
michael@0 | 380 | }); |
michael@0 | 381 | |
michael@0 | 382 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 383 | //// Helper functions |
michael@0 | 384 | |
michael@0 | 385 | /** |
michael@0 | 386 | * Joins multiple sql tokens into a single sql query. |
michael@0 | 387 | */ |
michael@0 | 388 | function sql(...parts) parts.join(" "); |
michael@0 | 389 | |
michael@0 | 390 | /** |
michael@0 | 391 | * Used to unescape encoded URI strings and drop information that we do not |
michael@0 | 392 | * care about. |
michael@0 | 393 | * |
michael@0 | 394 | * @param spec |
michael@0 | 395 | * The text to unescape and modify. |
michael@0 | 396 | * @return the modified spec. |
michael@0 | 397 | */ |
michael@0 | 398 | function fixupSearchText(spec) |
michael@0 | 399 | textURIService.unEscapeURIForUI("UTF-8", stripPrefix(spec)); |
michael@0 | 400 | |
michael@0 | 401 | /** |
michael@0 | 402 | * Generates the tokens used in searching from a given string. |
michael@0 | 403 | * |
michael@0 | 404 | * @param searchString |
michael@0 | 405 | * The string to generate tokens from. |
michael@0 | 406 | * @return an array of tokens. |
michael@0 | 407 | * @note Calling split on an empty string will return an array containing one |
michael@0 | 408 | * empty string. We don't want that, as it'll break our logic, so return |
michael@0 | 409 | * an empty array then. |
michael@0 | 410 | */ |
michael@0 | 411 | function getUnfilteredSearchTokens(searchString) |
michael@0 | 412 | searchString.length ? searchString.split(" ") : []; |
michael@0 | 413 | |
michael@0 | 414 | /** |
michael@0 | 415 | * Strip prefixes from the URI that we don't care about for searching. |
michael@0 | 416 | * |
michael@0 | 417 | * @param spec |
michael@0 | 418 | * The text to modify. |
michael@0 | 419 | * @return the modified spec. |
michael@0 | 420 | */ |
michael@0 | 421 | function stripPrefix(spec) |
michael@0 | 422 | { |
michael@0 | 423 | ["http://", "https://", "ftp://"].some(scheme => { |
michael@0 | 424 | if (spec.startsWith(scheme)) { |
michael@0 | 425 | spec = spec.slice(scheme.length); |
michael@0 | 426 | return true; |
michael@0 | 427 | } |
michael@0 | 428 | return false; |
michael@0 | 429 | }); |
michael@0 | 430 | |
michael@0 | 431 | if (spec.startsWith("www.")) { |
michael@0 | 432 | spec = spec.slice(4); |
michael@0 | 433 | } |
michael@0 | 434 | return spec; |
michael@0 | 435 | } |
michael@0 | 436 | |
michael@0 | 437 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 438 | //// Search Class |
michael@0 | 439 | //// Manages a single instance of an autocomplete search. |
michael@0 | 440 | |
michael@0 | 441 | function Search(searchString, searchParam, autocompleteListener, |
michael@0 | 442 | resultListener, autocompleteSearch) { |
michael@0 | 443 | // We want to store the original string with no leading or trailing |
michael@0 | 444 | // whitespace for case sensitive searches. |
michael@0 | 445 | this._originalSearchString = searchString.trim(); |
michael@0 | 446 | this._searchString = fixupSearchText(this._originalSearchString.toLowerCase()); |
michael@0 | 447 | this._searchTokens = |
michael@0 | 448 | this.filterTokens(getUnfilteredSearchTokens(this._searchString)); |
michael@0 | 449 | // The protocol and the host are lowercased by nsIURI, so it's fine to |
michael@0 | 450 | // lowercase the typed prefix, to add it back to the results later. |
michael@0 | 451 | this._strippedPrefix = this._originalSearchString.slice( |
michael@0 | 452 | 0, this._originalSearchString.length - this._searchString.length |
michael@0 | 453 | ).toLowerCase(); |
michael@0 | 454 | // The URIs in the database are fixed-up, so we can match on a lowercased |
michael@0 | 455 | // host, but the path must be matched in a case sensitive way. |
michael@0 | 456 | let pathIndex = |
michael@0 | 457 | this._originalSearchString.indexOf("/", this._strippedPrefix.length); |
michael@0 | 458 | this._autofillUrlSearchString = fixupSearchText( |
michael@0 | 459 | this._originalSearchString.slice(0, pathIndex).toLowerCase() + |
michael@0 | 460 | this._originalSearchString.slice(pathIndex) |
michael@0 | 461 | ); |
michael@0 | 462 | |
michael@0 | 463 | this._enableActions = searchParam.split(" ").indexOf("enable-actions") != -1; |
michael@0 | 464 | |
michael@0 | 465 | this._listener = autocompleteListener; |
michael@0 | 466 | this._autocompleteSearch = autocompleteSearch; |
michael@0 | 467 | |
michael@0 | 468 | this._matchBehavior = Prefs.matchBehavior; |
michael@0 | 469 | // Set the default behavior for this search. |
michael@0 | 470 | this._behavior = this._searchString ? Prefs.defaultBehavior |
michael@0 | 471 | : Prefs.emptySearchDefaultBehavior; |
michael@0 | 472 | // Create a new result to add eventual matches. Note we need a result |
michael@0 | 473 | // regardless having matches. |
michael@0 | 474 | let result = Cc["@mozilla.org/autocomplete/simple-result;1"] |
michael@0 | 475 | .createInstance(Ci.nsIAutoCompleteSimpleResult); |
michael@0 | 476 | result.setSearchString(searchString); |
michael@0 | 477 | result.setListener(resultListener); |
michael@0 | 478 | // Will be set later, if needed. |
michael@0 | 479 | result.setDefaultIndex(-1); |
michael@0 | 480 | this._result = result; |
michael@0 | 481 | |
michael@0 | 482 | // These are used to avoid adding duplicate entries to the results. |
michael@0 | 483 | this._usedURLs = new Set(); |
michael@0 | 484 | this._usedPlaceIds = new Set(); |
michael@0 | 485 | } |
michael@0 | 486 | |
michael@0 | 487 | Search.prototype = { |
michael@0 | 488 | /** |
michael@0 | 489 | * Enables the desired AutoComplete behavior. |
michael@0 | 490 | * |
michael@0 | 491 | * @param type |
michael@0 | 492 | * The behavior type to set. |
michael@0 | 493 | */ |
michael@0 | 494 | setBehavior: function (type) { |
michael@0 | 495 | this._behavior |= |
michael@0 | 496 | Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()]; |
michael@0 | 497 | }, |
michael@0 | 498 | |
michael@0 | 499 | /** |
michael@0 | 500 | * Determines if the specified AutoComplete behavior is set. |
michael@0 | 501 | * |
michael@0 | 502 | * @param aType |
michael@0 | 503 | * The behavior type to test for. |
michael@0 | 504 | * @return true if the behavior is set, false otherwise. |
michael@0 | 505 | */ |
michael@0 | 506 | hasBehavior: function (type) { |
michael@0 | 507 | return this._behavior & |
michael@0 | 508 | Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()]; |
michael@0 | 509 | }, |
michael@0 | 510 | |
michael@0 | 511 | /** |
michael@0 | 512 | * Used to delay the most complex queries, to save IO while the user is |
michael@0 | 513 | * typing. |
michael@0 | 514 | */ |
michael@0 | 515 | _sleepDeferred: null, |
michael@0 | 516 | _sleep: function (aTimeMs) { |
michael@0 | 517 | // Reuse a single instance to try shaving off some usless work before |
michael@0 | 518 | // the first query. |
michael@0 | 519 | if (!this._sleepTimer) |
michael@0 | 520 | this._sleepTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); |
michael@0 | 521 | this._sleepDeferred = Promise.defer(); |
michael@0 | 522 | this._sleepTimer.initWithCallback(() => this._sleepDeferred.resolve(), |
michael@0 | 523 | aTimeMs, Ci.nsITimer.TYPE_ONE_SHOT); |
michael@0 | 524 | return this._sleepDeferred.promise; |
michael@0 | 525 | }, |
michael@0 | 526 | |
michael@0 | 527 | /** |
michael@0 | 528 | * Given an array of tokens, this function determines which query should be |
michael@0 | 529 | * ran. It also removes any special search tokens. |
michael@0 | 530 | * |
michael@0 | 531 | * @param tokens |
michael@0 | 532 | * An array of search tokens. |
michael@0 | 533 | * @return the filtered list of tokens to search with. |
michael@0 | 534 | */ |
michael@0 | 535 | filterTokens: function (tokens) { |
michael@0 | 536 | // Set the proper behavior while filtering tokens. |
michael@0 | 537 | for (let i = tokens.length - 1; i >= 0; i--) { |
michael@0 | 538 | let behavior = Prefs.tokenToBehaviorMap.get(tokens[i]); |
michael@0 | 539 | // Don't remove the token if it didn't match, or if it's an action but |
michael@0 | 540 | // actions are not enabled. |
michael@0 | 541 | if (behavior && (behavior != "openpage" || this._enableActions)) { |
michael@0 | 542 | this.setBehavior(behavior); |
michael@0 | 543 | tokens.splice(i, 1); |
michael@0 | 544 | } |
michael@0 | 545 | } |
michael@0 | 546 | |
michael@0 | 547 | // Set the right JavaScript behavior based on our preference. Note that the |
michael@0 | 548 | // preference is whether or not we should filter JavaScript, and the |
michael@0 | 549 | // behavior is if we should search it or not. |
michael@0 | 550 | if (!Prefs.filterJavaScript) { |
michael@0 | 551 | this.setBehavior("javascript"); |
michael@0 | 552 | } |
michael@0 | 553 | |
michael@0 | 554 | return tokens; |
michael@0 | 555 | }, |
michael@0 | 556 | |
michael@0 | 557 | /** |
michael@0 | 558 | * Used to cancel this search, will stop providing results. |
michael@0 | 559 | */ |
michael@0 | 560 | cancel: function () { |
michael@0 | 561 | if (this._sleepTimer) |
michael@0 | 562 | this._sleepTimer.cancel(); |
michael@0 | 563 | if (this._sleepDeferred) { |
michael@0 | 564 | this._sleepDeferred.resolve(); |
michael@0 | 565 | this._sleepDeferred = null; |
michael@0 | 566 | } |
michael@0 | 567 | delete this._pendingQuery; |
michael@0 | 568 | }, |
michael@0 | 569 | |
michael@0 | 570 | /** |
michael@0 | 571 | * Whether this search is running. |
michael@0 | 572 | */ |
michael@0 | 573 | get pending() !!this._pendingQuery, |
michael@0 | 574 | |
michael@0 | 575 | /** |
michael@0 | 576 | * Execute the search and populate results. |
michael@0 | 577 | * @param conn |
michael@0 | 578 | * The Sqlite connection. |
michael@0 | 579 | */ |
michael@0 | 580 | execute: Task.async(function* (conn) { |
michael@0 | 581 | this._pendingQuery = true; |
michael@0 | 582 | TelemetryStopwatch.start(TELEMETRY_1ST_RESULT); |
michael@0 | 583 | |
michael@0 | 584 | // For any given search, we run many queries: |
michael@0 | 585 | // 1) priority domains |
michael@0 | 586 | // 2) inline completion |
michael@0 | 587 | // 3) keywords (this._keywordQuery) |
michael@0 | 588 | // 4) adaptive learning (this._adaptiveQuery) |
michael@0 | 589 | // 5) open pages not supported by history (this._switchToTabQuery) |
michael@0 | 590 | // 6) query based on match behavior |
michael@0 | 591 | // |
michael@0 | 592 | // (3) only gets ran if we get any filtered tokens, since if there are no |
michael@0 | 593 | // tokens, there is nothing to match. |
michael@0 | 594 | |
michael@0 | 595 | // Get the final query, based on the tokens found in the search string. |
michael@0 | 596 | let queries = [ this._adaptiveQuery, |
michael@0 | 597 | this._switchToTabQuery, |
michael@0 | 598 | this._searchQuery ]; |
michael@0 | 599 | |
michael@0 | 600 | if (this._searchTokens.length == 1) { |
michael@0 | 601 | yield this._matchPriorityUrl(); |
michael@0 | 602 | } else if (this._searchTokens.length > 1) { |
michael@0 | 603 | queries.unshift(this._keywordQuery); |
michael@0 | 604 | } |
michael@0 | 605 | |
michael@0 | 606 | if (this._shouldAutofill) { |
michael@0 | 607 | // Hosts have no "/" in them. |
michael@0 | 608 | let lastSlashIndex = this._searchString.lastIndexOf("/"); |
michael@0 | 609 | // Search only URLs if there's a slash in the search string... |
michael@0 | 610 | if (lastSlashIndex != -1) { |
michael@0 | 611 | // ...but not if it's exactly at the end of the search string. |
michael@0 | 612 | if (lastSlashIndex < this._searchString.length - 1) { |
michael@0 | 613 | queries.unshift(this._urlQuery); |
michael@0 | 614 | } |
michael@0 | 615 | } else if (this.pending) { |
michael@0 | 616 | // The host query is executed immediately, while any other is delayed |
michael@0 | 617 | // to avoid overloading the connection. |
michael@0 | 618 | let [ query, params ] = this._hostQuery; |
michael@0 | 619 | yield conn.executeCached(query, params, this._onResultRow.bind(this)); |
michael@0 | 620 | } |
michael@0 | 621 | } |
michael@0 | 622 | |
michael@0 | 623 | yield this._sleep(Prefs.delay); |
michael@0 | 624 | if (!this.pending) |
michael@0 | 625 | return; |
michael@0 | 626 | |
michael@0 | 627 | for (let [query, params] of queries) { |
michael@0 | 628 | yield conn.executeCached(query, params, this._onResultRow.bind(this)); |
michael@0 | 629 | if (!this.pending) |
michael@0 | 630 | return; |
michael@0 | 631 | } |
michael@0 | 632 | |
michael@0 | 633 | // If we do not have enough results, and our match type is |
michael@0 | 634 | // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more |
michael@0 | 635 | // results. |
michael@0 | 636 | if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE && |
michael@0 | 637 | this._result.matchCount < Prefs.maxRichResults) { |
michael@0 | 638 | this._matchBehavior = MATCH_ANYWHERE; |
michael@0 | 639 | for (let [query, params] of [ this._adaptiveQuery, |
michael@0 | 640 | this._searchQuery ]) { |
michael@0 | 641 | yield conn.executeCached(query, params, this._onResultRow); |
michael@0 | 642 | if (!this.pending) |
michael@0 | 643 | return; |
michael@0 | 644 | } |
michael@0 | 645 | } |
michael@0 | 646 | |
michael@0 | 647 | // If we didn't find enough matches and we have some frecency-driven |
michael@0 | 648 | // matches, add them. |
michael@0 | 649 | if (this._frecencyMatches) { |
michael@0 | 650 | this._frecencyMatches.forEach(this._addMatch, this); |
michael@0 | 651 | } |
michael@0 | 652 | }), |
michael@0 | 653 | |
michael@0 | 654 | _matchPriorityUrl: function* () { |
michael@0 | 655 | if (!Prefs.autofillPriority) |
michael@0 | 656 | return; |
michael@0 | 657 | let priorityMatch = yield PriorityUrlProvider.getMatch(this._searchString); |
michael@0 | 658 | if (priorityMatch) { |
michael@0 | 659 | this._result.setDefaultIndex(0); |
michael@0 | 660 | this._addFrecencyMatch({ |
michael@0 | 661 | value: priorityMatch.token, |
michael@0 | 662 | comment: priorityMatch.title, |
michael@0 | 663 | icon: priorityMatch.iconUrl, |
michael@0 | 664 | style: "priority-" + priorityMatch.reason, |
michael@0 | 665 | finalCompleteValue: priorityMatch.url, |
michael@0 | 666 | frecency: FRECENCY_PRIORITY_DEFAULT |
michael@0 | 667 | }); |
michael@0 | 668 | } |
michael@0 | 669 | }, |
michael@0 | 670 | |
michael@0 | 671 | _onResultRow: function (row) { |
michael@0 | 672 | TelemetryStopwatch.finish(TELEMETRY_1ST_RESULT); |
michael@0 | 673 | let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE); |
michael@0 | 674 | let match; |
michael@0 | 675 | switch (queryType) { |
michael@0 | 676 | case QUERYTYPE_AUTOFILL_HOST: |
michael@0 | 677 | this._result.setDefaultIndex(0); |
michael@0 | 678 | match = this._processHostRow(row); |
michael@0 | 679 | break; |
michael@0 | 680 | case QUERYTYPE_AUTOFILL_URL: |
michael@0 | 681 | this._result.setDefaultIndex(0); |
michael@0 | 682 | match = this._processUrlRow(row); |
michael@0 | 683 | break; |
michael@0 | 684 | case QUERYTYPE_FILTERED: |
michael@0 | 685 | case QUERYTYPE_KEYWORD: |
michael@0 | 686 | match = this._processRow(row); |
michael@0 | 687 | break; |
michael@0 | 688 | } |
michael@0 | 689 | this._addMatch(match); |
michael@0 | 690 | }, |
michael@0 | 691 | |
michael@0 | 692 | /** |
michael@0 | 693 | * These matches should be mixed up with other matches, based on frecency. |
michael@0 | 694 | */ |
michael@0 | 695 | _addFrecencyMatch: function (match) { |
michael@0 | 696 | if (!this._frecencyMatches) |
michael@0 | 697 | this._frecencyMatches = []; |
michael@0 | 698 | this._frecencyMatches.push(match); |
michael@0 | 699 | // We keep this array in reverse order, so we can walk it and remove stuff |
michael@0 | 700 | // from it in one pass. Notice that for frecency reverse order means from |
michael@0 | 701 | // lower to higher. |
michael@0 | 702 | this._frecencyMatches.sort((a, b) => a.frecency - b.frecency); |
michael@0 | 703 | }, |
michael@0 | 704 | |
michael@0 | 705 | _addMatch: function (match) { |
michael@0 | 706 | let notifyResults = false; |
michael@0 | 707 | |
michael@0 | 708 | if (this._frecencyMatches) { |
michael@0 | 709 | for (let i = this._frecencyMatches.length - 1; i >= 0 ; i--) { |
michael@0 | 710 | if (this._frecencyMatches[i].frecency > match.frecency) { |
michael@0 | 711 | this._addMatch(this._frecencyMatches.splice(i, 1)[0]); |
michael@0 | 712 | } |
michael@0 | 713 | } |
michael@0 | 714 | } |
michael@0 | 715 | |
michael@0 | 716 | // Must check both id and url, cause keywords dinamically modify the url. |
michael@0 | 717 | if ((!match.placeId || !this._usedPlaceIds.has(match.placeId)) && |
michael@0 | 718 | !this._usedURLs.has(stripPrefix(match.value))) { |
michael@0 | 719 | // Add this to our internal tracker to ensure duplicates do not end up in |
michael@0 | 720 | // the result. |
michael@0 | 721 | // Not all entries have a place id, thus we fallback to the url for them. |
michael@0 | 722 | // We cannot use only the url since keywords entries are modified to |
michael@0 | 723 | // include the search string, and would be returned multiple times. Ids |
michael@0 | 724 | // are faster too. |
michael@0 | 725 | if (match.placeId) |
michael@0 | 726 | this._usedPlaceIds.add(match.placeId); |
michael@0 | 727 | this._usedURLs.add(stripPrefix(match.value)); |
michael@0 | 728 | |
michael@0 | 729 | this._result.appendMatch(match.value, |
michael@0 | 730 | match.comment, |
michael@0 | 731 | match.icon || PlacesUtils.favicons.defaultFavicon.spec, |
michael@0 | 732 | match.style || "favicon", |
michael@0 | 733 | match.finalCompleteValue); |
michael@0 | 734 | notifyResults = true; |
michael@0 | 735 | } |
michael@0 | 736 | |
michael@0 | 737 | if (this._result.matchCount == Prefs.maxRichResults || !this.pending) { |
michael@0 | 738 | // We have enough results, so stop running our search. |
michael@0 | 739 | this.cancel(); |
michael@0 | 740 | // This tells Sqlite.jsm to stop providing us results and cancel the |
michael@0 | 741 | // underlying query. |
michael@0 | 742 | throw StopIteration; |
michael@0 | 743 | } |
michael@0 | 744 | |
michael@0 | 745 | if (notifyResults) { |
michael@0 | 746 | // Notify about results if we've gotten them. |
michael@0 | 747 | this.notifyResults(true); |
michael@0 | 748 | } |
michael@0 | 749 | }, |
michael@0 | 750 | |
michael@0 | 751 | _processHostRow: function (row) { |
michael@0 | 752 | let match = {}; |
michael@0 | 753 | let trimmedHost = row.getResultByIndex(QUERYINDEX_URL); |
michael@0 | 754 | let untrimmedHost = row.getResultByIndex(QUERYINDEX_TITLE); |
michael@0 | 755 | let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY); |
michael@0 | 756 | // If the untrimmed value doesn't preserve the user's input just |
michael@0 | 757 | // ignore it and complete to the found host. |
michael@0 | 758 | if (untrimmedHost && |
michael@0 | 759 | !untrimmedHost.toLowerCase().contains(this._originalSearchString.toLowerCase())) { |
michael@0 | 760 | // THIS CAUSES null TO BE SHOWN AS TITLE. |
michael@0 | 761 | untrimmedHost = null; |
michael@0 | 762 | } |
michael@0 | 763 | |
michael@0 | 764 | match.value = this._strippedPrefix + trimmedHost; |
michael@0 | 765 | match.comment = trimmedHost; |
michael@0 | 766 | match.finalCompleteValue = untrimmedHost; |
michael@0 | 767 | match.frecency = frecency; |
michael@0 | 768 | return match; |
michael@0 | 769 | }, |
michael@0 | 770 | |
michael@0 | 771 | _processUrlRow: function (row) { |
michael@0 | 772 | let match = {}; |
michael@0 | 773 | let value = row.getResultByIndex(QUERYINDEX_URL); |
michael@0 | 774 | let url = fixupSearchText(value); |
michael@0 | 775 | let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY); |
michael@0 | 776 | |
michael@0 | 777 | let prefix = value.slice(0, value.length - stripPrefix(value).length); |
michael@0 | 778 | |
michael@0 | 779 | // We must complete the URL up to the next separator (which is /, ? or #). |
michael@0 | 780 | let separatorIndex = url.slice(this._searchString.length) |
michael@0 | 781 | .search(/[\/\?\#]/); |
michael@0 | 782 | if (separatorIndex != -1) { |
michael@0 | 783 | separatorIndex += this._searchString.length; |
michael@0 | 784 | if (url[separatorIndex] == "/") { |
michael@0 | 785 | separatorIndex++; // Include the "/" separator |
michael@0 | 786 | } |
michael@0 | 787 | url = url.slice(0, separatorIndex); |
michael@0 | 788 | } |
michael@0 | 789 | |
michael@0 | 790 | // If the untrimmed value doesn't preserve the user's input just |
michael@0 | 791 | // ignore it and complete to the found url. |
michael@0 | 792 | let untrimmedURL = prefix + url; |
michael@0 | 793 | if (untrimmedURL && |
michael@0 | 794 | !untrimmedURL.toLowerCase().contains(this._originalSearchString.toLowerCase())) { |
michael@0 | 795 | // THIS CAUSES null TO BE SHOWN AS TITLE. |
michael@0 | 796 | untrimmedURL = null; |
michael@0 | 797 | } |
michael@0 | 798 | |
michael@0 | 799 | match.value = this._strippedPrefix + url; |
michael@0 | 800 | match.comment = url; |
michael@0 | 801 | match.finalCompleteValue = untrimmedURL; |
michael@0 | 802 | match.frecency = frecency; |
michael@0 | 803 | return match; |
michael@0 | 804 | }, |
michael@0 | 805 | |
michael@0 | 806 | _processRow: function (row) { |
michael@0 | 807 | let match = {}; |
michael@0 | 808 | match.placeId = row.getResultByIndex(QUERYINDEX_PLACEID); |
michael@0 | 809 | let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE); |
michael@0 | 810 | let escapedURL = row.getResultByIndex(QUERYINDEX_URL); |
michael@0 | 811 | let openPageCount = row.getResultByIndex(QUERYINDEX_SWITCHTAB) || 0; |
michael@0 | 812 | let historyTitle = row.getResultByIndex(QUERYINDEX_TITLE) || ""; |
michael@0 | 813 | let iconurl = row.getResultByIndex(QUERYINDEX_ICONURL) || ""; |
michael@0 | 814 | let bookmarked = row.getResultByIndex(QUERYINDEX_BOOKMARKED); |
michael@0 | 815 | let bookmarkTitle = bookmarked ? |
michael@0 | 816 | row.getResultByIndex(QUERYINDEX_BOOKMARKTITLE) : null; |
michael@0 | 817 | let tags = row.getResultByIndex(QUERYINDEX_TAGS) || ""; |
michael@0 | 818 | let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY); |
michael@0 | 819 | |
michael@0 | 820 | // If actions are enabled and the page is open, add only the switch-to-tab |
michael@0 | 821 | // result. Otherwise, add the normal result. |
michael@0 | 822 | let [url, action] = this._enableActions && openPageCount > 0 ? |
michael@0 | 823 | ["moz-action:switchtab," + escapedURL, "action "] : |
michael@0 | 824 | [escapedURL, ""]; |
michael@0 | 825 | |
michael@0 | 826 | // Always prefer the bookmark title unless it is empty |
michael@0 | 827 | let title = bookmarkTitle || historyTitle; |
michael@0 | 828 | |
michael@0 | 829 | if (queryType == QUERYTYPE_KEYWORD) { |
michael@0 | 830 | // If we do not have a title, then we must have a keyword, so let the UI |
michael@0 | 831 | // know it is a keyword. Otherwise, we found an exact page match, so just |
michael@0 | 832 | // show the page like a regular result. Because the page title is likely |
michael@0 | 833 | // going to be more specific than the bookmark title (keyword title). |
michael@0 | 834 | if (!historyTitle) { |
michael@0 | 835 | match.style = "keyword"; |
michael@0 | 836 | } |
michael@0 | 837 | else { |
michael@0 | 838 | title = historyTitle; |
michael@0 | 839 | } |
michael@0 | 840 | } |
michael@0 | 841 | |
michael@0 | 842 | // We will always prefer to show tags if we have them. |
michael@0 | 843 | let showTags = !!tags; |
michael@0 | 844 | |
michael@0 | 845 | // However, we'll act as if a page is not bookmarked or tagged if the user |
michael@0 | 846 | // only wants only history and not bookmarks or tags. |
michael@0 | 847 | if (this.hasBehavior("history") && |
michael@0 | 848 | !(this.hasBehavior("bookmark") || this.hasBehavior("tag"))) { |
michael@0 | 849 | showTags = false; |
michael@0 | 850 | match.style = "favicon"; |
michael@0 | 851 | } |
michael@0 | 852 | |
michael@0 | 853 | // If we have tags and should show them, we need to add them to the title. |
michael@0 | 854 | if (showTags) { |
michael@0 | 855 | title += TITLE_TAGS_SEPARATOR + tags; |
michael@0 | 856 | } |
michael@0 | 857 | |
michael@0 | 858 | // We have to determine the right style to display. Tags show the tag icon, |
michael@0 | 859 | // bookmarks get the bookmark icon, and keywords get the keyword icon. If |
michael@0 | 860 | // the result does not fall into any of those, it just gets the favicon. |
michael@0 | 861 | if (!match.style) { |
michael@0 | 862 | // It is possible that we already have a style set (from a keyword |
michael@0 | 863 | // search or because of the user's preferences), so only set it if we |
michael@0 | 864 | // haven't already done so. |
michael@0 | 865 | if (showTags) { |
michael@0 | 866 | match.style = "tag"; |
michael@0 | 867 | } |
michael@0 | 868 | else if (bookmarked) { |
michael@0 | 869 | match.style = "bookmark"; |
michael@0 | 870 | } |
michael@0 | 871 | } |
michael@0 | 872 | |
michael@0 | 873 | match.value = url; |
michael@0 | 874 | match.comment = title; |
michael@0 | 875 | if (iconurl) { |
michael@0 | 876 | match.icon = PlacesUtils.favicons |
michael@0 | 877 | .getFaviconLinkForIcon(NetUtil.newURI(iconurl)).spec; |
michael@0 | 878 | } |
michael@0 | 879 | match.frecency = frecency; |
michael@0 | 880 | |
michael@0 | 881 | return match; |
michael@0 | 882 | }, |
michael@0 | 883 | |
michael@0 | 884 | /** |
michael@0 | 885 | * Obtains the search query to be used based on the previously set search |
michael@0 | 886 | * behaviors (accessed by this.hasBehavior). |
michael@0 | 887 | * |
michael@0 | 888 | * @return an array consisting of the correctly optimized query to search the |
michael@0 | 889 | * database with and an object containing the params to bound. |
michael@0 | 890 | */ |
michael@0 | 891 | get _searchQuery() { |
michael@0 | 892 | // We use more optimized queries for restricted searches, so we will always |
michael@0 | 893 | // return the most restrictive one to the least restrictive one if more than |
michael@0 | 894 | // one token is found. |
michael@0 | 895 | // Note: "openpages" behavior is supported by the default query. |
michael@0 | 896 | // _switchToTabQuery instead returns only pages not supported by |
michael@0 | 897 | // history and it is always executed. |
michael@0 | 898 | let query = this.hasBehavior("tag") ? SQL_TAGS_QUERY : |
michael@0 | 899 | this.hasBehavior("bookmark") ? SQL_BOOKMARK_QUERY : |
michael@0 | 900 | this.hasBehavior("typed") ? SQL_TYPED_QUERY : |
michael@0 | 901 | this.hasBehavior("history") ? SQL_HISTORY_QUERY : |
michael@0 | 902 | SQL_DEFAULT_QUERY; |
michael@0 | 903 | |
michael@0 | 904 | return [ |
michael@0 | 905 | query, |
michael@0 | 906 | { |
michael@0 | 907 | parent: PlacesUtils.tagsFolderId, |
michael@0 | 908 | query_type: QUERYTYPE_FILTERED, |
michael@0 | 909 | matchBehavior: this._matchBehavior, |
michael@0 | 910 | searchBehavior: this._behavior, |
michael@0 | 911 | // We only want to search the tokens that we are left with - not the |
michael@0 | 912 | // original search string. |
michael@0 | 913 | searchString: this._searchTokens.join(" "), |
michael@0 | 914 | // Limit the query to the the maximum number of desired results. |
michael@0 | 915 | // This way we can avoid doing more work than needed. |
michael@0 | 916 | maxResults: Prefs.maxRichResults |
michael@0 | 917 | } |
michael@0 | 918 | ]; |
michael@0 | 919 | }, |
michael@0 | 920 | |
michael@0 | 921 | /** |
michael@0 | 922 | * Obtains the query to search for keywords. |
michael@0 | 923 | * |
michael@0 | 924 | * @return an array consisting of the correctly optimized query to search the |
michael@0 | 925 | * database with and an object containing the params to bound. |
michael@0 | 926 | */ |
michael@0 | 927 | get _keywordQuery() { |
michael@0 | 928 | // The keyword is the first word in the search string, with the parameters |
michael@0 | 929 | // following it. |
michael@0 | 930 | let searchString = this._originalSearchString; |
michael@0 | 931 | let queryString = ""; |
michael@0 | 932 | let queryIndex = searchString.indexOf(" "); |
michael@0 | 933 | if (queryIndex != -1) { |
michael@0 | 934 | queryString = searchString.substring(queryIndex + 1); |
michael@0 | 935 | } |
michael@0 | 936 | // We need to escape the parameters as if they were the query in a URL |
michael@0 | 937 | queryString = encodeURIComponent(queryString).replace("%20", "+", "g"); |
michael@0 | 938 | |
michael@0 | 939 | // The first word could be a keyword, so that's what we'll search. |
michael@0 | 940 | let keyword = this._searchTokens[0]; |
michael@0 | 941 | |
michael@0 | 942 | return [ |
michael@0 | 943 | SQL_KEYWORD_QUERY, |
michael@0 | 944 | { |
michael@0 | 945 | keyword: keyword, |
michael@0 | 946 | query_string: queryString, |
michael@0 | 947 | query_type: QUERYTYPE_KEYWORD |
michael@0 | 948 | } |
michael@0 | 949 | ]; |
michael@0 | 950 | }, |
michael@0 | 951 | |
michael@0 | 952 | /** |
michael@0 | 953 | * Obtains the query to search for switch-to-tab entries. |
michael@0 | 954 | * |
michael@0 | 955 | * @return an array consisting of the correctly optimized query to search the |
michael@0 | 956 | * database with and an object containing the params to bound. |
michael@0 | 957 | */ |
michael@0 | 958 | get _switchToTabQuery() [ |
michael@0 | 959 | SQL_SWITCHTAB_QUERY, |
michael@0 | 960 | { |
michael@0 | 961 | query_type: QUERYTYPE_FILTERED, |
michael@0 | 962 | matchBehavior: this._matchBehavior, |
michael@0 | 963 | searchBehavior: this._behavior, |
michael@0 | 964 | // We only want to search the tokens that we are left with - not the |
michael@0 | 965 | // original search string. |
michael@0 | 966 | searchString: this._searchTokens.join(" "), |
michael@0 | 967 | maxResults: Prefs.maxRichResults |
michael@0 | 968 | } |
michael@0 | 969 | ], |
michael@0 | 970 | |
michael@0 | 971 | /** |
michael@0 | 972 | * Obtains the query to search for adaptive results. |
michael@0 | 973 | * |
michael@0 | 974 | * @return an array consisting of the correctly optimized query to search the |
michael@0 | 975 | * database with and an object containing the params to bound. |
michael@0 | 976 | */ |
michael@0 | 977 | get _adaptiveQuery() [ |
michael@0 | 978 | SQL_ADAPTIVE_QUERY, |
michael@0 | 979 | { |
michael@0 | 980 | parent: PlacesUtils.tagsFolderId, |
michael@0 | 981 | search_string: this._searchString, |
michael@0 | 982 | query_type: QUERYTYPE_FILTERED, |
michael@0 | 983 | matchBehavior: this._matchBehavior, |
michael@0 | 984 | searchBehavior: this._behavior |
michael@0 | 985 | } |
michael@0 | 986 | ], |
michael@0 | 987 | |
michael@0 | 988 | /** |
michael@0 | 989 | * Whether we should try to autoFill. |
michael@0 | 990 | */ |
michael@0 | 991 | get _shouldAutofill() { |
michael@0 | 992 | // First of all, check for the autoFill pref. |
michael@0 | 993 | if (!Prefs.autofill) |
michael@0 | 994 | return false; |
michael@0 | 995 | |
michael@0 | 996 | // Then, we should not try to autofill if the behavior is not the default. |
michael@0 | 997 | // TODO (bug 751709): Ideally we should have a more fine-grained behavior |
michael@0 | 998 | // here, but for now it's enough to just check for default behavior. |
michael@0 | 999 | if (Prefs.defaultBehavior != DEFAULT_BEHAVIOR) |
michael@0 | 1000 | return false; |
michael@0 | 1001 | |
michael@0 | 1002 | // Don't autoFill if the search term is recognized as a keyword, otherwise |
michael@0 | 1003 | // it will override default keywords behavior. Note that keywords are |
michael@0 | 1004 | // hashed on first use, so while the first query may delay a little bit, |
michael@0 | 1005 | // next ones will just hit the memory hash. |
michael@0 | 1006 | if (this._searchString.length == 0 || |
michael@0 | 1007 | PlacesUtils.bookmarks.getURIForKeyword(this._searchString)) { |
michael@0 | 1008 | return false; |
michael@0 | 1009 | } |
michael@0 | 1010 | |
michael@0 | 1011 | // Don't try to autofill if the search term includes any whitespace. |
michael@0 | 1012 | // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH |
michael@0 | 1013 | // tokenizer ends up trimming the search string and returning a value |
michael@0 | 1014 | // that doesn't match it, or is even shorter. |
michael@0 | 1015 | if (/\s/.test(this._searchString)) { |
michael@0 | 1016 | return false; |
michael@0 | 1017 | } |
michael@0 | 1018 | |
michael@0 | 1019 | return true; |
michael@0 | 1020 | }, |
michael@0 | 1021 | |
michael@0 | 1022 | /** |
michael@0 | 1023 | * Obtains the query to search for autoFill host results. |
michael@0 | 1024 | * |
michael@0 | 1025 | * @return an array consisting of the correctly optimized query to search the |
michael@0 | 1026 | * database with and an object containing the params to bound. |
michael@0 | 1027 | */ |
michael@0 | 1028 | get _hostQuery() [ |
michael@0 | 1029 | Prefs.autofillTyped ? SQL_TYPED_HOST_QUERY : SQL_TYPED_QUERY, |
michael@0 | 1030 | { |
michael@0 | 1031 | query_type: QUERYTYPE_AUTOFILL_HOST, |
michael@0 | 1032 | searchString: this._searchString.toLowerCase() |
michael@0 | 1033 | } |
michael@0 | 1034 | ], |
michael@0 | 1035 | |
michael@0 | 1036 | /** |
michael@0 | 1037 | * Obtains the query to search for autoFill url results. |
michael@0 | 1038 | * |
michael@0 | 1039 | * @return an array consisting of the correctly optimized query to search the |
michael@0 | 1040 | * database with and an object containing the params to bound. |
michael@0 | 1041 | */ |
michael@0 | 1042 | get _urlQuery() [ |
michael@0 | 1043 | Prefs.autofillTyped ? SQL_TYPED_HOST_QUERY : SQL_TYPED_QUERY, |
michael@0 | 1044 | { |
michael@0 | 1045 | query_type: QUERYTYPE_AUTOFILL_URL, |
michael@0 | 1046 | searchString: this._autofillUrlSearchString, |
michael@0 | 1047 | matchBehavior: MATCH_BEGINNING_CASE_SENSITIVE, |
michael@0 | 1048 | searchBehavior: Ci.mozIPlacesAutoComplete.BEHAVIOR_URL |
michael@0 | 1049 | } |
michael@0 | 1050 | ], |
michael@0 | 1051 | |
michael@0 | 1052 | /** |
michael@0 | 1053 | * Notifies the listener about results. |
michael@0 | 1054 | * |
michael@0 | 1055 | * @param searchOngoing |
michael@0 | 1056 | * Indicates whether the search is ongoing. |
michael@0 | 1057 | */ |
michael@0 | 1058 | notifyResults: function (searchOngoing) { |
michael@0 | 1059 | let result = this._result; |
michael@0 | 1060 | let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH"; |
michael@0 | 1061 | if (searchOngoing) { |
michael@0 | 1062 | resultCode += "_ONGOING"; |
michael@0 | 1063 | } |
michael@0 | 1064 | result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]); |
michael@0 | 1065 | this._listener.onSearchResult(this._autocompleteSearch, result); |
michael@0 | 1066 | }, |
michael@0 | 1067 | } |
michael@0 | 1068 | |
michael@0 | 1069 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1070 | //// UnifiedComplete class |
michael@0 | 1071 | //// component @mozilla.org/autocomplete/search;1?name=unifiedcomplete |
michael@0 | 1072 | |
michael@0 | 1073 | function UnifiedComplete() { |
michael@0 | 1074 | Services.obs.addObserver(this, TOPIC_SHUTDOWN, true); |
michael@0 | 1075 | } |
michael@0 | 1076 | |
michael@0 | 1077 | UnifiedComplete.prototype = { |
michael@0 | 1078 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1079 | //// nsIObserver |
michael@0 | 1080 | |
michael@0 | 1081 | observe: function (subject, topic, data) { |
michael@0 | 1082 | if (topic === TOPIC_SHUTDOWN) { |
michael@0 | 1083 | this.ensureShutdown(); |
michael@0 | 1084 | } |
michael@0 | 1085 | }, |
michael@0 | 1086 | |
michael@0 | 1087 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1088 | //// Database handling |
michael@0 | 1089 | |
michael@0 | 1090 | /** |
michael@0 | 1091 | * Promise resolved when the database initialization has completed, or null |
michael@0 | 1092 | * if it has never been requested. |
michael@0 | 1093 | */ |
michael@0 | 1094 | _promiseDatabase: null, |
michael@0 | 1095 | |
michael@0 | 1096 | /** |
michael@0 | 1097 | * Gets a Sqlite database handle. |
michael@0 | 1098 | * |
michael@0 | 1099 | * @return {Promise} |
michael@0 | 1100 | * @resolves to the Sqlite database handle (according to Sqlite.jsm). |
michael@0 | 1101 | * @rejects javascript exception. |
michael@0 | 1102 | */ |
michael@0 | 1103 | getDatabaseHandle: function () { |
michael@0 | 1104 | if (Prefs.enabled && !this._promiseDatabase) { |
michael@0 | 1105 | this._promiseDatabase = Task.spawn(function* () { |
michael@0 | 1106 | let conn = yield Sqlite.cloneStorageConnection({ |
michael@0 | 1107 | connection: PlacesUtils.history.DBConnection, |
michael@0 | 1108 | readOnly: true |
michael@0 | 1109 | }); |
michael@0 | 1110 | |
michael@0 | 1111 | // Autocomplete often fallbacks to a table scan due to lack of text |
michael@0 | 1112 | // indices. A larger cache helps reducing IO and improving performance. |
michael@0 | 1113 | // The value used here is larger than the default Storage value defined |
michael@0 | 1114 | // as MAX_CACHE_SIZE_BYTES in storage/src/mozStorageConnection.cpp. |
michael@0 | 1115 | yield conn.execute("PRAGMA cache_size = -6144"); // 6MiB |
michael@0 | 1116 | |
michael@0 | 1117 | yield SwitchToTabStorage.initDatabase(conn); |
michael@0 | 1118 | |
michael@0 | 1119 | return conn; |
michael@0 | 1120 | }.bind(this)).then(null, Cu.reportError); |
michael@0 | 1121 | } |
michael@0 | 1122 | return this._promiseDatabase; |
michael@0 | 1123 | }, |
michael@0 | 1124 | |
michael@0 | 1125 | /** |
michael@0 | 1126 | * Used to stop running queries and close the database handle. |
michael@0 | 1127 | */ |
michael@0 | 1128 | ensureShutdown: function () { |
michael@0 | 1129 | if (this._promiseDatabase) { |
michael@0 | 1130 | Task.spawn(function* () { |
michael@0 | 1131 | let conn = yield this.getDatabaseHandle(); |
michael@0 | 1132 | SwitchToTabStorage.shutdown(); |
michael@0 | 1133 | yield conn.close() |
michael@0 | 1134 | }.bind(this)).then(null, Cu.reportError); |
michael@0 | 1135 | this._promiseDatabase = null; |
michael@0 | 1136 | } |
michael@0 | 1137 | }, |
michael@0 | 1138 | |
michael@0 | 1139 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1140 | //// mozIPlacesAutoComplete |
michael@0 | 1141 | |
michael@0 | 1142 | registerOpenPage: function PAC_registerOpenPage(uri) { |
michael@0 | 1143 | SwitchToTabStorage.add(uri); |
michael@0 | 1144 | }, |
michael@0 | 1145 | |
michael@0 | 1146 | unregisterOpenPage: function PAC_unregisterOpenPage(uri) { |
michael@0 | 1147 | SwitchToTabStorage.delete(uri); |
michael@0 | 1148 | }, |
michael@0 | 1149 | |
michael@0 | 1150 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1151 | //// nsIAutoCompleteSearch |
michael@0 | 1152 | |
michael@0 | 1153 | startSearch: function (searchString, searchParam, previousResult, listener) { |
michael@0 | 1154 | // Stop the search in case the controller has not taken care of it. |
michael@0 | 1155 | if (this._currentSearch) { |
michael@0 | 1156 | this.stopSearch(); |
michael@0 | 1157 | } |
michael@0 | 1158 | |
michael@0 | 1159 | // Note: We don't use previousResult to make sure ordering of results are |
michael@0 | 1160 | // consistent. See bug 412730 for more details. |
michael@0 | 1161 | |
michael@0 | 1162 | this._currentSearch = new Search(searchString, searchParam, listener, |
michael@0 | 1163 | this, this); |
michael@0 | 1164 | |
michael@0 | 1165 | // If we are not enabled, we need to return now. Notice we need an empty |
michael@0 | 1166 | // result regardless, so we still create the Search object. |
michael@0 | 1167 | if (!Prefs.enabled) { |
michael@0 | 1168 | this.finishSearch(true); |
michael@0 | 1169 | return; |
michael@0 | 1170 | } |
michael@0 | 1171 | |
michael@0 | 1172 | let search = this._currentSearch; |
michael@0 | 1173 | this.getDatabaseHandle().then(conn => search.execute(conn)) |
michael@0 | 1174 | .then(() => { |
michael@0 | 1175 | if (search == this._currentSearch) { |
michael@0 | 1176 | this.finishSearch(true); |
michael@0 | 1177 | } |
michael@0 | 1178 | }, Cu.reportError); |
michael@0 | 1179 | }, |
michael@0 | 1180 | |
michael@0 | 1181 | stopSearch: function () { |
michael@0 | 1182 | if (this._currentSearch) { |
michael@0 | 1183 | this._currentSearch.cancel(); |
michael@0 | 1184 | } |
michael@0 | 1185 | this.finishSearch(); |
michael@0 | 1186 | }, |
michael@0 | 1187 | |
michael@0 | 1188 | /** |
michael@0 | 1189 | * Properly cleans up when searching is completed. |
michael@0 | 1190 | * |
michael@0 | 1191 | * @param notify [optional] |
michael@0 | 1192 | * Indicates if we should notify the AutoComplete listener about our |
michael@0 | 1193 | * results or not. |
michael@0 | 1194 | */ |
michael@0 | 1195 | finishSearch: function (notify=false) { |
michael@0 | 1196 | // Notify about results if we are supposed to. |
michael@0 | 1197 | if (notify) { |
michael@0 | 1198 | this._currentSearch.notifyResults(false); |
michael@0 | 1199 | } |
michael@0 | 1200 | |
michael@0 | 1201 | // Clear our state |
michael@0 | 1202 | TelemetryStopwatch.cancel(TELEMETRY_1ST_RESULT); |
michael@0 | 1203 | delete this._currentSearch; |
michael@0 | 1204 | }, |
michael@0 | 1205 | |
michael@0 | 1206 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1207 | //// nsIAutoCompleteSimpleResultListener |
michael@0 | 1208 | |
michael@0 | 1209 | onValueRemoved: function (result, spec, removeFromDB) { |
michael@0 | 1210 | if (removeFromDB) { |
michael@0 | 1211 | PlacesUtils.history.removePage(NetUtil.newURI(spec)); |
michael@0 | 1212 | } |
michael@0 | 1213 | }, |
michael@0 | 1214 | |
michael@0 | 1215 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1216 | //// nsIAutoCompleteSearchDescriptor |
michael@0 | 1217 | |
michael@0 | 1218 | get searchType() Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE, |
michael@0 | 1219 | |
michael@0 | 1220 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1221 | //// nsISupports |
michael@0 | 1222 | |
michael@0 | 1223 | classID: Components.ID("f964a319-397a-4d21-8be6-5cdd1ee3e3ae"), |
michael@0 | 1224 | |
michael@0 | 1225 | _xpcom_factory: XPCOMUtils.generateSingletonFactory(UnifiedComplete), |
michael@0 | 1226 | |
michael@0 | 1227 | QueryInterface: XPCOMUtils.generateQI([ |
michael@0 | 1228 | Ci.nsIAutoCompleteSearch, |
michael@0 | 1229 | Ci.nsIAutoCompleteSimpleResultListener, |
michael@0 | 1230 | Ci.mozIPlacesAutoComplete, |
michael@0 | 1231 | Ci.nsIObserver, |
michael@0 | 1232 | Ci.nsISupportsWeakReference |
michael@0 | 1233 | ]) |
michael@0 | 1234 | }; |
michael@0 | 1235 | |
michael@0 | 1236 | this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UnifiedComplete]); |