toolkit/components/places/UnifiedComplete.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

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]);

mercurial