1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/places/UnifiedComplete.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1236 @@ 1.4 +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- 1.5 + * vim: sw=2 ts=2 sts=2 expandtab 1.6 + * This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +"use strict"; 1.11 + 1.12 +//////////////////////////////////////////////////////////////////////////////// 1.13 +//// Constants 1.14 + 1.15 +const Cc = Components.classes; 1.16 +const Ci = Components.interfaces; 1.17 +const Cr = Components.results; 1.18 +const Cu = Components.utils; 1.19 + 1.20 +const TOPIC_SHUTDOWN = "places-shutdown"; 1.21 +const TOPIC_PREFCHANGED = "nsPref:changed"; 1.22 + 1.23 +const DEFAULT_BEHAVIOR = 0; 1.24 + 1.25 +const PREF_BRANCH = "browser.urlbar"; 1.26 + 1.27 +// Prefs are defined as [pref name, default value]. 1.28 +const PREF_ENABLED = [ "autocomplete.enabled", true ]; 1.29 +const PREF_AUTOFILL = [ "autoFill", true ]; 1.30 +const PREF_AUTOFILL_TYPED = [ "autoFill.typed", true ]; 1.31 +const PREF_AUTOFILL_PRIORITY = [ "autoFill.priority", true ]; 1.32 +const PREF_DELAY = [ "delay", 50 ]; 1.33 +const PREF_BEHAVIOR = [ "matchBehavior", MATCH_BOUNDARY_ANYWHERE ]; 1.34 +const PREF_DEFAULT_BEHAVIOR = [ "default.behavior", DEFAULT_BEHAVIOR ]; 1.35 +const PREF_EMPTY_BEHAVIOR = [ "default.behavior.emptyRestriction", 1.36 + Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY | 1.37 + Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED ]; 1.38 +const PREF_FILTER_JS = [ "filter.javascript", true ]; 1.39 +const PREF_MAXRESULTS = [ "maxRichResults", 25 ]; 1.40 +const PREF_RESTRICT_HISTORY = [ "restrict.history", "^" ]; 1.41 +const PREF_RESTRICT_BOOKMARKS = [ "restrict.bookmark", "*" ]; 1.42 +const PREF_RESTRICT_TYPED = [ "restrict.typed", "~" ]; 1.43 +const PREF_RESTRICT_TAG = [ "restrict.tag", "+" ]; 1.44 +const PREF_RESTRICT_SWITCHTAB = [ "restrict.openpage", "%" ]; 1.45 +const PREF_MATCH_TITLE = [ "match.title", "#" ]; 1.46 +const PREF_MATCH_URL = [ "match.url", "@" ]; 1.47 + 1.48 +// Match type constants. 1.49 +// These indicate what type of search function we should be using. 1.50 +const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE; 1.51 +const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE; 1.52 +const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY; 1.53 +const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING; 1.54 +const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE; 1.55 + 1.56 +// AutoComplete query type constants. 1.57 +// Describes the various types of queries that we can process rows for. 1.58 +const QUERYTYPE_KEYWORD = 0; 1.59 +const QUERYTYPE_FILTERED = 1; 1.60 +const QUERYTYPE_AUTOFILL_HOST = 2; 1.61 +const QUERYTYPE_AUTOFILL_URL = 3; 1.62 + 1.63 +// This separator is used as an RTL-friendly way to split the title and tags. 1.64 +// It can also be used by an nsIAutoCompleteResult consumer to re-split the 1.65 +// "comment" back into the title and the tag. 1.66 +const TITLE_TAGS_SEPARATOR = " \u2013 "; 1.67 + 1.68 +// Telemetry probes. 1.69 +const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS"; 1.70 + 1.71 +// The default frecency value used when inserting priority results. 1.72 +const FRECENCY_PRIORITY_DEFAULT = 1000; 1.73 + 1.74 +// Sqlite result row index constants. 1.75 +const QUERYINDEX_QUERYTYPE = 0; 1.76 +const QUERYINDEX_URL = 1; 1.77 +const QUERYINDEX_TITLE = 2; 1.78 +const QUERYINDEX_ICONURL = 3; 1.79 +const QUERYINDEX_BOOKMARKED = 4; 1.80 +const QUERYINDEX_BOOKMARKTITLE = 5; 1.81 +const QUERYINDEX_TAGS = 6; 1.82 +const QUERYINDEX_VISITCOUNT = 7; 1.83 +const QUERYINDEX_TYPED = 8; 1.84 +const QUERYINDEX_PLACEID = 9; 1.85 +const QUERYINDEX_SWITCHTAB = 10; 1.86 +const QUERYINDEX_FRECENCY = 11; 1.87 + 1.88 +// This SQL query fragment provides the following: 1.89 +// - whether the entry is bookmarked (QUERYINDEX_BOOKMARKED) 1.90 +// - the bookmark title, if it is a bookmark (QUERYINDEX_BOOKMARKTITLE) 1.91 +// - the tags associated with a bookmarked entry (QUERYINDEX_TAGS) 1.92 +const SQL_BOOKMARK_TAGS_FRAGMENT = sql( 1.93 + "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked,", 1.94 + "( SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL", 1.95 + "ORDER BY lastModified DESC LIMIT 1", 1.96 + ") AS btitle,", 1.97 + "( SELECT GROUP_CONCAT(t.title, ',')", 1.98 + "FROM moz_bookmarks b", 1.99 + "JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent", 1.100 + "WHERE b.fk = h.id", 1.101 + ") AS tags"); 1.102 + 1.103 +// TODO bug 412736: in case of a frecency tie, we might break it with h.typed 1.104 +// and h.visit_count. That is slower though, so not doing it yet... 1.105 +const SQL_DEFAULT_QUERY = sql( 1.106 + "SELECT :query_type, h.url, h.title, f.url,", SQL_BOOKMARK_TAGS_FRAGMENT, ",", 1.107 + "h.visit_count, h.typed, h.id, t.open_count, h.frecency", 1.108 + "FROM moz_places h", 1.109 + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id", 1.110 + "LEFT JOIN moz_openpages_temp t ON t.url = h.url", 1.111 + "WHERE h.frecency <> 0", 1.112 + "AND AUTOCOMPLETE_MATCH(:searchString, h.url,", 1.113 + "IFNULL(btitle, h.title), tags,", 1.114 + "h.visit_count, h.typed,", 1.115 + "bookmarked, t.open_count,", 1.116 + ":matchBehavior, :searchBehavior)", 1.117 + "/*CONDITIONS*/", 1.118 + "ORDER BY h.frecency DESC, h.id DESC", 1.119 + "LIMIT :maxResults"); 1.120 + 1.121 +// Enforce ignoring the visit_count index, since the frecency one is much 1.122 +// faster in this case. ANALYZE helps the query planner to figure out the 1.123 +// faster path, but it may not have up-to-date information yet. 1.124 +const SQL_HISTORY_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/", 1.125 + "AND +h.visit_count > 0", "g"); 1.126 + 1.127 +const SQL_BOOKMARK_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/", 1.128 + "AND bookmarked", "g"); 1.129 + 1.130 +const SQL_TAGS_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/", 1.131 + "AND tags NOTNULL", "g"); 1.132 + 1.133 +const SQL_TYPED_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/", 1.134 + "AND h.typed = 1", "g"); 1.135 + 1.136 +const SQL_SWITCHTAB_QUERY = sql( 1.137 + "SELECT :query_type, t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL,", 1.138 + "t.open_count, NULL", 1.139 + "FROM moz_openpages_temp t", 1.140 + "LEFT JOIN moz_places h ON h.url = t.url", 1.141 + "WHERE h.id IS NULL", 1.142 + "AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL,", 1.143 + "NULL, NULL, NULL, t.open_count,", 1.144 + ":matchBehavior, :searchBehavior)", 1.145 + "ORDER BY t.ROWID DESC", 1.146 + "LIMIT :maxResults"); 1.147 + 1.148 +const SQL_ADAPTIVE_QUERY = sql( 1.149 + "/* do not warn (bug 487789) */", 1.150 + "SELECT :query_type, h.url, h.title, f.url,", SQL_BOOKMARK_TAGS_FRAGMENT, ",", 1.151 + "h.visit_count, h.typed, h.id, t.open_count, h.frecency", 1.152 + "FROM (", 1.153 + "SELECT ROUND(MAX(use_count) * (1 + (input = :search_string)), 1) AS rank,", 1.154 + "place_id", 1.155 + "FROM moz_inputhistory", 1.156 + "WHERE input BETWEEN :search_string AND :search_string || X'FFFF'", 1.157 + "GROUP BY place_id", 1.158 + ") AS i", 1.159 + "JOIN moz_places h ON h.id = i.place_id", 1.160 + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id", 1.161 + "LEFT JOIN moz_openpages_temp t ON t.url = h.url", 1.162 + "WHERE AUTOCOMPLETE_MATCH(NULL, h.url,", 1.163 + "IFNULL(btitle, h.title), tags,", 1.164 + "h.visit_count, h.typed, bookmarked,", 1.165 + "t.open_count,", 1.166 + ":matchBehavior, :searchBehavior)", 1.167 + "ORDER BY rank DESC, h.frecency DESC"); 1.168 + 1.169 +const SQL_KEYWORD_QUERY = sql( 1.170 + "/* do not warn (bug 487787) */", 1.171 + "SELECT :query_type,", 1.172 + "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk)", 1.173 + "AS search_url, h.title,", 1.174 + "IFNULL(f.url, (SELECT f.url", 1.175 + "FROM moz_places", 1.176 + "JOIN moz_favicons f ON f.id = favicon_id", 1.177 + "WHERE rev_host = (SELECT rev_host FROM moz_places WHERE id = b.fk)", 1.178 + "ORDER BY frecency DESC", 1.179 + "LIMIT 1)", 1.180 + "),", 1.181 + "1, b.title, NULL, h.visit_count, h.typed, IFNULL(h.id, b.fk),", 1.182 + "t.open_count, h.frecency", 1.183 + "FROM moz_keywords k", 1.184 + "JOIN moz_bookmarks b ON b.keyword_id = k.id", 1.185 + "LEFT JOIN moz_places h ON h.url = search_url", 1.186 + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id", 1.187 + "LEFT JOIN moz_openpages_temp t ON t.url = search_url", 1.188 + "WHERE LOWER(k.keyword) = LOWER(:keyword)", 1.189 + "ORDER BY h.frecency DESC"); 1.190 + 1.191 +const SQL_HOST_QUERY = sql( 1.192 + "/* do not warn (bug NA): not worth to index on (typed, frecency) */", 1.193 + "SELECT :query_type, host || '/', prefix || host || '/',", 1.194 + "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, frecency", 1.195 + "FROM moz_hosts", 1.196 + "WHERE host BETWEEN :searchString AND :searchString || X'FFFF'", 1.197 + "AND frecency <> 0", 1.198 + "/*CONDITIONS*/", 1.199 + "ORDER BY frecency DESC", 1.200 + "LIMIT 1"); 1.201 + 1.202 +const SQL_TYPED_HOST_QUERY = SQL_HOST_QUERY.replace("/*CONDITIONS*/", 1.203 + "AND typed = 1"); 1.204 +const SQL_URL_QUERY = sql( 1.205 + "/* do not warn (bug no): cannot use an index */", 1.206 + "SELECT :query_type, h.url,", 1.207 + "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, h.frecency", 1.208 + "FROM moz_places h", 1.209 + "WHERE h.frecency <> 0", 1.210 + "/*CONDITIONS*/", 1.211 + "AND AUTOCOMPLETE_MATCH(:searchString, h.url,", 1.212 + "h.title, '',", 1.213 + "h.visit_count, h.typed, 0, 0,", 1.214 + ":matchBehavior, :searchBehavior)", 1.215 + "ORDER BY h.frecency DESC, h.id DESC", 1.216 + "LIMIT 1"); 1.217 + 1.218 +const SQL_TYPED_URL_QUERY = SQL_URL_QUERY.replace("/*CONDITIONS*/", 1.219 + "AND typed = 1"); 1.220 + 1.221 +//////////////////////////////////////////////////////////////////////////////// 1.222 +//// Getters 1.223 + 1.224 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.225 +Cu.import("resource://gre/modules/Services.jsm"); 1.226 + 1.227 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", 1.228 + "resource://gre/modules/PlacesUtils.jsm"); 1.229 +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", 1.230 + "resource://gre/modules/TelemetryStopwatch.jsm"); 1.231 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", 1.232 + "resource://gre/modules/NetUtil.jsm"); 1.233 +XPCOMUtils.defineLazyModuleGetter(this, "Preferences", 1.234 + "resource://gre/modules/Preferences.jsm"); 1.235 +XPCOMUtils.defineLazyModuleGetter(this, "Sqlite", 1.236 + "resource://gre/modules/Sqlite.jsm"); 1.237 +XPCOMUtils.defineLazyModuleGetter(this, "OS", 1.238 + "resource://gre/modules/osfile.jsm"); 1.239 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.240 + "resource://gre/modules/Promise.jsm"); 1.241 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.242 + "resource://gre/modules/Task.jsm"); 1.243 +XPCOMUtils.defineLazyModuleGetter(this, "PriorityUrlProvider", 1.244 + "resource://gre/modules/PriorityUrlProvider.jsm"); 1.245 + 1.246 +XPCOMUtils.defineLazyServiceGetter(this, "textURIService", 1.247 + "@mozilla.org/intl/texttosuburi;1", 1.248 + "nsITextToSubURI"); 1.249 + 1.250 +/** 1.251 + * Storage object for switch-to-tab entries. 1.252 + * This takes care of caching and registering open pages, that will be reused 1.253 + * by switch-to-tab queries. It has an internal cache, so that the Sqlite 1.254 + * store is lazy initialized only on first use. 1.255 + * It has a simple API: 1.256 + * initDatabase(conn): initializes the temporary Sqlite entities to store data 1.257 + * add(uri): adds a given nsIURI to the store 1.258 + * delete(uri): removes a given nsIURI from the store 1.259 + * shutdown(): stops storing data to Sqlite 1.260 + */ 1.261 +XPCOMUtils.defineLazyGetter(this, "SwitchToTabStorage", () => Object.seal({ 1.262 + _conn: null, 1.263 + // Temporary queue used while the database connection is not available. 1.264 + _queue: new Set(), 1.265 + initDatabase: Task.async(function* (conn) { 1.266 + // To reduce IO use an in-memory table for switch-to-tab tracking. 1.267 + // Note: this should be kept up-to-date with the definition in 1.268 + // nsPlacesTables.h. 1.269 + yield conn.execute(sql( 1.270 + "CREATE TEMP TABLE moz_openpages_temp (", 1.271 + "url TEXT PRIMARY KEY,", 1.272 + "open_count INTEGER", 1.273 + ")")); 1.274 + 1.275 + // Note: this should be kept up-to-date with the definition in 1.276 + // nsPlacesTriggers.h. 1.277 + yield conn.execute(sql( 1.278 + "CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger", 1.279 + "AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW", 1.280 + "WHEN NEW.open_count = 0", 1.281 + "BEGIN", 1.282 + "DELETE FROM moz_openpages_temp", 1.283 + "WHERE url = NEW.url;", 1.284 + "END")); 1.285 + 1.286 + this._conn = conn; 1.287 + 1.288 + // Populate the table with the current cache contents... 1.289 + this._queue.forEach(this.add, this); 1.290 + // ...then clear it to avoid double additions. 1.291 + this._queue.clear(); 1.292 + }), 1.293 + 1.294 + add: function (uri) { 1.295 + if (!this._conn) { 1.296 + this._queue.add(uri); 1.297 + return; 1.298 + } 1.299 + this._conn.executeCached(sql( 1.300 + "INSERT OR REPLACE INTO moz_openpages_temp (url, open_count)", 1.301 + "VALUES ( :url, IFNULL( (SELECT open_count + 1", 1.302 + "FROM moz_openpages_temp", 1.303 + "WHERE url = :url),", 1.304 + "1", 1.305 + ")", 1.306 + ")" 1.307 + ), { url: uri.spec }); 1.308 + }, 1.309 + 1.310 + delete: function (uri) { 1.311 + if (!this._conn) { 1.312 + this._queue.delete(uri); 1.313 + return; 1.314 + } 1.315 + this._conn.executeCached(sql( 1.316 + "UPDATE moz_openpages_temp", 1.317 + "SET open_count = open_count - 1", 1.318 + "WHERE url = :url" 1.319 + ), { url: uri.spec }); 1.320 + }, 1.321 + 1.322 + shutdown: function () { 1.323 + this._conn = null; 1.324 + this._queue.clear(); 1.325 + } 1.326 +})); 1.327 + 1.328 +/** 1.329 + * This helper keeps track of preferences and keeps their values up-to-date. 1.330 + */ 1.331 +XPCOMUtils.defineLazyGetter(this, "Prefs", () => { 1.332 + let prefs = new Preferences(PREF_BRANCH); 1.333 + 1.334 + function loadPrefs() { 1.335 + store.enabled = prefs.get(...PREF_ENABLED); 1.336 + store.autofill = prefs.get(...PREF_AUTOFILL); 1.337 + store.autofillTyped = prefs.get(...PREF_AUTOFILL_TYPED); 1.338 + store.autofillPriority = prefs.get(...PREF_AUTOFILL_PRIORITY); 1.339 + store.delay = prefs.get(...PREF_DELAY); 1.340 + store.matchBehavior = prefs.get(...PREF_BEHAVIOR); 1.341 + store.filterJavaScript = prefs.get(...PREF_FILTER_JS); 1.342 + store.maxRichResults = prefs.get(...PREF_MAXRESULTS); 1.343 + store.restrictHistoryToken = prefs.get(...PREF_RESTRICT_HISTORY); 1.344 + store.restrictBookmarkToken = prefs.get(...PREF_RESTRICT_BOOKMARKS); 1.345 + store.restrictTypedToken = prefs.get(...PREF_RESTRICT_TYPED); 1.346 + store.restrictTagToken = prefs.get(...PREF_RESTRICT_TAG); 1.347 + store.restrictOpenPageToken = prefs.get(...PREF_RESTRICT_SWITCHTAB); 1.348 + store.matchTitleToken = prefs.get(...PREF_MATCH_TITLE); 1.349 + store.matchURLToken = prefs.get(...PREF_MATCH_URL); 1.350 + store.defaultBehavior = prefs.get(...PREF_DEFAULT_BEHAVIOR); 1.351 + // Further restrictions to apply for "empty searches" (i.e. searches for ""). 1.352 + store.emptySearchDefaultBehavior = store.defaultBehavior | 1.353 + prefs.get(...PREF_EMPTY_BEHAVIOR); 1.354 + 1.355 + // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE. 1.356 + if (store.matchBehavior != MATCH_ANYWHERE && 1.357 + store.matchBehavior != MATCH_BOUNDARY && 1.358 + store.matchBehavior != MATCH_BEGINNING) { 1.359 + store.matchBehavior = MATCH_BOUNDARY_ANYWHERE; 1.360 + } 1.361 + 1.362 + store.tokenToBehaviorMap = new Map([ 1.363 + [ store.restrictHistoryToken, "history" ], 1.364 + [ store.restrictBookmarkToken, "bookmark" ], 1.365 + [ store.restrictTagToken, "tag" ], 1.366 + [ store.restrictOpenPageToken, "openpage" ], 1.367 + [ store.matchTitleToken, "title" ], 1.368 + [ store.matchURLToken, "url" ], 1.369 + [ store.restrictTypedToken, "typed" ] 1.370 + ]); 1.371 + } 1.372 + 1.373 + let store = { 1.374 + observe: function (subject, topic, data) { 1.375 + loadPrefs(); 1.376 + }, 1.377 + QueryInterface: XPCOMUtils.generateQI([ Ci.nsIObserver ]) 1.378 + }; 1.379 + loadPrefs(); 1.380 + prefs.observe("", store); 1.381 + 1.382 + return Object.seal(store); 1.383 +}); 1.384 + 1.385 +//////////////////////////////////////////////////////////////////////////////// 1.386 +//// Helper functions 1.387 + 1.388 +/** 1.389 + * Joins multiple sql tokens into a single sql query. 1.390 + */ 1.391 +function sql(...parts) parts.join(" "); 1.392 + 1.393 +/** 1.394 + * Used to unescape encoded URI strings and drop information that we do not 1.395 + * care about. 1.396 + * 1.397 + * @param spec 1.398 + * The text to unescape and modify. 1.399 + * @return the modified spec. 1.400 + */ 1.401 +function fixupSearchText(spec) 1.402 + textURIService.unEscapeURIForUI("UTF-8", stripPrefix(spec)); 1.403 + 1.404 +/** 1.405 + * Generates the tokens used in searching from a given string. 1.406 + * 1.407 + * @param searchString 1.408 + * The string to generate tokens from. 1.409 + * @return an array of tokens. 1.410 + * @note Calling split on an empty string will return an array containing one 1.411 + * empty string. We don't want that, as it'll break our logic, so return 1.412 + * an empty array then. 1.413 + */ 1.414 +function getUnfilteredSearchTokens(searchString) 1.415 + searchString.length ? searchString.split(" ") : []; 1.416 + 1.417 +/** 1.418 + * Strip prefixes from the URI that we don't care about for searching. 1.419 + * 1.420 + * @param spec 1.421 + * The text to modify. 1.422 + * @return the modified spec. 1.423 + */ 1.424 +function stripPrefix(spec) 1.425 +{ 1.426 + ["http://", "https://", "ftp://"].some(scheme => { 1.427 + if (spec.startsWith(scheme)) { 1.428 + spec = spec.slice(scheme.length); 1.429 + return true; 1.430 + } 1.431 + return false; 1.432 + }); 1.433 + 1.434 + if (spec.startsWith("www.")) { 1.435 + spec = spec.slice(4); 1.436 + } 1.437 + return spec; 1.438 +} 1.439 + 1.440 +//////////////////////////////////////////////////////////////////////////////// 1.441 +//// Search Class 1.442 +//// Manages a single instance of an autocomplete search. 1.443 + 1.444 +function Search(searchString, searchParam, autocompleteListener, 1.445 + resultListener, autocompleteSearch) { 1.446 + // We want to store the original string with no leading or trailing 1.447 + // whitespace for case sensitive searches. 1.448 + this._originalSearchString = searchString.trim(); 1.449 + this._searchString = fixupSearchText(this._originalSearchString.toLowerCase()); 1.450 + this._searchTokens = 1.451 + this.filterTokens(getUnfilteredSearchTokens(this._searchString)); 1.452 + // The protocol and the host are lowercased by nsIURI, so it's fine to 1.453 + // lowercase the typed prefix, to add it back to the results later. 1.454 + this._strippedPrefix = this._originalSearchString.slice( 1.455 + 0, this._originalSearchString.length - this._searchString.length 1.456 + ).toLowerCase(); 1.457 + // The URIs in the database are fixed-up, so we can match on a lowercased 1.458 + // host, but the path must be matched in a case sensitive way. 1.459 + let pathIndex = 1.460 + this._originalSearchString.indexOf("/", this._strippedPrefix.length); 1.461 + this._autofillUrlSearchString = fixupSearchText( 1.462 + this._originalSearchString.slice(0, pathIndex).toLowerCase() + 1.463 + this._originalSearchString.slice(pathIndex) 1.464 + ); 1.465 + 1.466 + this._enableActions = searchParam.split(" ").indexOf("enable-actions") != -1; 1.467 + 1.468 + this._listener = autocompleteListener; 1.469 + this._autocompleteSearch = autocompleteSearch; 1.470 + 1.471 + this._matchBehavior = Prefs.matchBehavior; 1.472 + // Set the default behavior for this search. 1.473 + this._behavior = this._searchString ? Prefs.defaultBehavior 1.474 + : Prefs.emptySearchDefaultBehavior; 1.475 + // Create a new result to add eventual matches. Note we need a result 1.476 + // regardless having matches. 1.477 + let result = Cc["@mozilla.org/autocomplete/simple-result;1"] 1.478 + .createInstance(Ci.nsIAutoCompleteSimpleResult); 1.479 + result.setSearchString(searchString); 1.480 + result.setListener(resultListener); 1.481 + // Will be set later, if needed. 1.482 + result.setDefaultIndex(-1); 1.483 + this._result = result; 1.484 + 1.485 + // These are used to avoid adding duplicate entries to the results. 1.486 + this._usedURLs = new Set(); 1.487 + this._usedPlaceIds = new Set(); 1.488 +} 1.489 + 1.490 +Search.prototype = { 1.491 + /** 1.492 + * Enables the desired AutoComplete behavior. 1.493 + * 1.494 + * @param type 1.495 + * The behavior type to set. 1.496 + */ 1.497 + setBehavior: function (type) { 1.498 + this._behavior |= 1.499 + Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()]; 1.500 + }, 1.501 + 1.502 + /** 1.503 + * Determines if the specified AutoComplete behavior is set. 1.504 + * 1.505 + * @param aType 1.506 + * The behavior type to test for. 1.507 + * @return true if the behavior is set, false otherwise. 1.508 + */ 1.509 + hasBehavior: function (type) { 1.510 + return this._behavior & 1.511 + Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()]; 1.512 + }, 1.513 + 1.514 + /** 1.515 + * Used to delay the most complex queries, to save IO while the user is 1.516 + * typing. 1.517 + */ 1.518 + _sleepDeferred: null, 1.519 + _sleep: function (aTimeMs) { 1.520 + // Reuse a single instance to try shaving off some usless work before 1.521 + // the first query. 1.522 + if (!this._sleepTimer) 1.523 + this._sleepTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 1.524 + this._sleepDeferred = Promise.defer(); 1.525 + this._sleepTimer.initWithCallback(() => this._sleepDeferred.resolve(), 1.526 + aTimeMs, Ci.nsITimer.TYPE_ONE_SHOT); 1.527 + return this._sleepDeferred.promise; 1.528 + }, 1.529 + 1.530 + /** 1.531 + * Given an array of tokens, this function determines which query should be 1.532 + * ran. It also removes any special search tokens. 1.533 + * 1.534 + * @param tokens 1.535 + * An array of search tokens. 1.536 + * @return the filtered list of tokens to search with. 1.537 + */ 1.538 + filterTokens: function (tokens) { 1.539 + // Set the proper behavior while filtering tokens. 1.540 + for (let i = tokens.length - 1; i >= 0; i--) { 1.541 + let behavior = Prefs.tokenToBehaviorMap.get(tokens[i]); 1.542 + // Don't remove the token if it didn't match, or if it's an action but 1.543 + // actions are not enabled. 1.544 + if (behavior && (behavior != "openpage" || this._enableActions)) { 1.545 + this.setBehavior(behavior); 1.546 + tokens.splice(i, 1); 1.547 + } 1.548 + } 1.549 + 1.550 + // Set the right JavaScript behavior based on our preference. Note that the 1.551 + // preference is whether or not we should filter JavaScript, and the 1.552 + // behavior is if we should search it or not. 1.553 + if (!Prefs.filterJavaScript) { 1.554 + this.setBehavior("javascript"); 1.555 + } 1.556 + 1.557 + return tokens; 1.558 + }, 1.559 + 1.560 + /** 1.561 + * Used to cancel this search, will stop providing results. 1.562 + */ 1.563 + cancel: function () { 1.564 + if (this._sleepTimer) 1.565 + this._sleepTimer.cancel(); 1.566 + if (this._sleepDeferred) { 1.567 + this._sleepDeferred.resolve(); 1.568 + this._sleepDeferred = null; 1.569 + } 1.570 + delete this._pendingQuery; 1.571 + }, 1.572 + 1.573 + /** 1.574 + * Whether this search is running. 1.575 + */ 1.576 + get pending() !!this._pendingQuery, 1.577 + 1.578 + /** 1.579 + * Execute the search and populate results. 1.580 + * @param conn 1.581 + * The Sqlite connection. 1.582 + */ 1.583 + execute: Task.async(function* (conn) { 1.584 + this._pendingQuery = true; 1.585 + TelemetryStopwatch.start(TELEMETRY_1ST_RESULT); 1.586 + 1.587 + // For any given search, we run many queries: 1.588 + // 1) priority domains 1.589 + // 2) inline completion 1.590 + // 3) keywords (this._keywordQuery) 1.591 + // 4) adaptive learning (this._adaptiveQuery) 1.592 + // 5) open pages not supported by history (this._switchToTabQuery) 1.593 + // 6) query based on match behavior 1.594 + // 1.595 + // (3) only gets ran if we get any filtered tokens, since if there are no 1.596 + // tokens, there is nothing to match. 1.597 + 1.598 + // Get the final query, based on the tokens found in the search string. 1.599 + let queries = [ this._adaptiveQuery, 1.600 + this._switchToTabQuery, 1.601 + this._searchQuery ]; 1.602 + 1.603 + if (this._searchTokens.length == 1) { 1.604 + yield this._matchPriorityUrl(); 1.605 + } else if (this._searchTokens.length > 1) { 1.606 + queries.unshift(this._keywordQuery); 1.607 + } 1.608 + 1.609 + if (this._shouldAutofill) { 1.610 + // Hosts have no "/" in them. 1.611 + let lastSlashIndex = this._searchString.lastIndexOf("/"); 1.612 + // Search only URLs if there's a slash in the search string... 1.613 + if (lastSlashIndex != -1) { 1.614 + // ...but not if it's exactly at the end of the search string. 1.615 + if (lastSlashIndex < this._searchString.length - 1) { 1.616 + queries.unshift(this._urlQuery); 1.617 + } 1.618 + } else if (this.pending) { 1.619 + // The host query is executed immediately, while any other is delayed 1.620 + // to avoid overloading the connection. 1.621 + let [ query, params ] = this._hostQuery; 1.622 + yield conn.executeCached(query, params, this._onResultRow.bind(this)); 1.623 + } 1.624 + } 1.625 + 1.626 + yield this._sleep(Prefs.delay); 1.627 + if (!this.pending) 1.628 + return; 1.629 + 1.630 + for (let [query, params] of queries) { 1.631 + yield conn.executeCached(query, params, this._onResultRow.bind(this)); 1.632 + if (!this.pending) 1.633 + return; 1.634 + } 1.635 + 1.636 + // If we do not have enough results, and our match type is 1.637 + // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more 1.638 + // results. 1.639 + if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE && 1.640 + this._result.matchCount < Prefs.maxRichResults) { 1.641 + this._matchBehavior = MATCH_ANYWHERE; 1.642 + for (let [query, params] of [ this._adaptiveQuery, 1.643 + this._searchQuery ]) { 1.644 + yield conn.executeCached(query, params, this._onResultRow); 1.645 + if (!this.pending) 1.646 + return; 1.647 + } 1.648 + } 1.649 + 1.650 + // If we didn't find enough matches and we have some frecency-driven 1.651 + // matches, add them. 1.652 + if (this._frecencyMatches) { 1.653 + this._frecencyMatches.forEach(this._addMatch, this); 1.654 + } 1.655 + }), 1.656 + 1.657 + _matchPriorityUrl: function* () { 1.658 + if (!Prefs.autofillPriority) 1.659 + return; 1.660 + let priorityMatch = yield PriorityUrlProvider.getMatch(this._searchString); 1.661 + if (priorityMatch) { 1.662 + this._result.setDefaultIndex(0); 1.663 + this._addFrecencyMatch({ 1.664 + value: priorityMatch.token, 1.665 + comment: priorityMatch.title, 1.666 + icon: priorityMatch.iconUrl, 1.667 + style: "priority-" + priorityMatch.reason, 1.668 + finalCompleteValue: priorityMatch.url, 1.669 + frecency: FRECENCY_PRIORITY_DEFAULT 1.670 + }); 1.671 + } 1.672 + }, 1.673 + 1.674 + _onResultRow: function (row) { 1.675 + TelemetryStopwatch.finish(TELEMETRY_1ST_RESULT); 1.676 + let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE); 1.677 + let match; 1.678 + switch (queryType) { 1.679 + case QUERYTYPE_AUTOFILL_HOST: 1.680 + this._result.setDefaultIndex(0); 1.681 + match = this._processHostRow(row); 1.682 + break; 1.683 + case QUERYTYPE_AUTOFILL_URL: 1.684 + this._result.setDefaultIndex(0); 1.685 + match = this._processUrlRow(row); 1.686 + break; 1.687 + case QUERYTYPE_FILTERED: 1.688 + case QUERYTYPE_KEYWORD: 1.689 + match = this._processRow(row); 1.690 + break; 1.691 + } 1.692 + this._addMatch(match); 1.693 + }, 1.694 + 1.695 + /** 1.696 + * These matches should be mixed up with other matches, based on frecency. 1.697 + */ 1.698 + _addFrecencyMatch: function (match) { 1.699 + if (!this._frecencyMatches) 1.700 + this._frecencyMatches = []; 1.701 + this._frecencyMatches.push(match); 1.702 + // We keep this array in reverse order, so we can walk it and remove stuff 1.703 + // from it in one pass. Notice that for frecency reverse order means from 1.704 + // lower to higher. 1.705 + this._frecencyMatches.sort((a, b) => a.frecency - b.frecency); 1.706 + }, 1.707 + 1.708 + _addMatch: function (match) { 1.709 + let notifyResults = false; 1.710 + 1.711 + if (this._frecencyMatches) { 1.712 + for (let i = this._frecencyMatches.length - 1; i >= 0 ; i--) { 1.713 + if (this._frecencyMatches[i].frecency > match.frecency) { 1.714 + this._addMatch(this._frecencyMatches.splice(i, 1)[0]); 1.715 + } 1.716 + } 1.717 + } 1.718 + 1.719 + // Must check both id and url, cause keywords dinamically modify the url. 1.720 + if ((!match.placeId || !this._usedPlaceIds.has(match.placeId)) && 1.721 + !this._usedURLs.has(stripPrefix(match.value))) { 1.722 + // Add this to our internal tracker to ensure duplicates do not end up in 1.723 + // the result. 1.724 + // Not all entries have a place id, thus we fallback to the url for them. 1.725 + // We cannot use only the url since keywords entries are modified to 1.726 + // include the search string, and would be returned multiple times. Ids 1.727 + // are faster too. 1.728 + if (match.placeId) 1.729 + this._usedPlaceIds.add(match.placeId); 1.730 + this._usedURLs.add(stripPrefix(match.value)); 1.731 + 1.732 + this._result.appendMatch(match.value, 1.733 + match.comment, 1.734 + match.icon || PlacesUtils.favicons.defaultFavicon.spec, 1.735 + match.style || "favicon", 1.736 + match.finalCompleteValue); 1.737 + notifyResults = true; 1.738 + } 1.739 + 1.740 + if (this._result.matchCount == Prefs.maxRichResults || !this.pending) { 1.741 + // We have enough results, so stop running our search. 1.742 + this.cancel(); 1.743 + // This tells Sqlite.jsm to stop providing us results and cancel the 1.744 + // underlying query. 1.745 + throw StopIteration; 1.746 + } 1.747 + 1.748 + if (notifyResults) { 1.749 + // Notify about results if we've gotten them. 1.750 + this.notifyResults(true); 1.751 + } 1.752 + }, 1.753 + 1.754 + _processHostRow: function (row) { 1.755 + let match = {}; 1.756 + let trimmedHost = row.getResultByIndex(QUERYINDEX_URL); 1.757 + let untrimmedHost = row.getResultByIndex(QUERYINDEX_TITLE); 1.758 + let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY); 1.759 + // If the untrimmed value doesn't preserve the user's input just 1.760 + // ignore it and complete to the found host. 1.761 + if (untrimmedHost && 1.762 + !untrimmedHost.toLowerCase().contains(this._originalSearchString.toLowerCase())) { 1.763 + // THIS CAUSES null TO BE SHOWN AS TITLE. 1.764 + untrimmedHost = null; 1.765 + } 1.766 + 1.767 + match.value = this._strippedPrefix + trimmedHost; 1.768 + match.comment = trimmedHost; 1.769 + match.finalCompleteValue = untrimmedHost; 1.770 + match.frecency = frecency; 1.771 + return match; 1.772 + }, 1.773 + 1.774 + _processUrlRow: function (row) { 1.775 + let match = {}; 1.776 + let value = row.getResultByIndex(QUERYINDEX_URL); 1.777 + let url = fixupSearchText(value); 1.778 + let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY); 1.779 + 1.780 + let prefix = value.slice(0, value.length - stripPrefix(value).length); 1.781 + 1.782 + // We must complete the URL up to the next separator (which is /, ? or #). 1.783 + let separatorIndex = url.slice(this._searchString.length) 1.784 + .search(/[\/\?\#]/); 1.785 + if (separatorIndex != -1) { 1.786 + separatorIndex += this._searchString.length; 1.787 + if (url[separatorIndex] == "/") { 1.788 + separatorIndex++; // Include the "/" separator 1.789 + } 1.790 + url = url.slice(0, separatorIndex); 1.791 + } 1.792 + 1.793 + // If the untrimmed value doesn't preserve the user's input just 1.794 + // ignore it and complete to the found url. 1.795 + let untrimmedURL = prefix + url; 1.796 + if (untrimmedURL && 1.797 + !untrimmedURL.toLowerCase().contains(this._originalSearchString.toLowerCase())) { 1.798 + // THIS CAUSES null TO BE SHOWN AS TITLE. 1.799 + untrimmedURL = null; 1.800 + } 1.801 + 1.802 + match.value = this._strippedPrefix + url; 1.803 + match.comment = url; 1.804 + match.finalCompleteValue = untrimmedURL; 1.805 + match.frecency = frecency; 1.806 + return match; 1.807 + }, 1.808 + 1.809 + _processRow: function (row) { 1.810 + let match = {}; 1.811 + match.placeId = row.getResultByIndex(QUERYINDEX_PLACEID); 1.812 + let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE); 1.813 + let escapedURL = row.getResultByIndex(QUERYINDEX_URL); 1.814 + let openPageCount = row.getResultByIndex(QUERYINDEX_SWITCHTAB) || 0; 1.815 + let historyTitle = row.getResultByIndex(QUERYINDEX_TITLE) || ""; 1.816 + let iconurl = row.getResultByIndex(QUERYINDEX_ICONURL) || ""; 1.817 + let bookmarked = row.getResultByIndex(QUERYINDEX_BOOKMARKED); 1.818 + let bookmarkTitle = bookmarked ? 1.819 + row.getResultByIndex(QUERYINDEX_BOOKMARKTITLE) : null; 1.820 + let tags = row.getResultByIndex(QUERYINDEX_TAGS) || ""; 1.821 + let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY); 1.822 + 1.823 + // If actions are enabled and the page is open, add only the switch-to-tab 1.824 + // result. Otherwise, add the normal result. 1.825 + let [url, action] = this._enableActions && openPageCount > 0 ? 1.826 + ["moz-action:switchtab," + escapedURL, "action "] : 1.827 + [escapedURL, ""]; 1.828 + 1.829 + // Always prefer the bookmark title unless it is empty 1.830 + let title = bookmarkTitle || historyTitle; 1.831 + 1.832 + if (queryType == QUERYTYPE_KEYWORD) { 1.833 + // If we do not have a title, then we must have a keyword, so let the UI 1.834 + // know it is a keyword. Otherwise, we found an exact page match, so just 1.835 + // show the page like a regular result. Because the page title is likely 1.836 + // going to be more specific than the bookmark title (keyword title). 1.837 + if (!historyTitle) { 1.838 + match.style = "keyword"; 1.839 + } 1.840 + else { 1.841 + title = historyTitle; 1.842 + } 1.843 + } 1.844 + 1.845 + // We will always prefer to show tags if we have them. 1.846 + let showTags = !!tags; 1.847 + 1.848 + // However, we'll act as if a page is not bookmarked or tagged if the user 1.849 + // only wants only history and not bookmarks or tags. 1.850 + if (this.hasBehavior("history") && 1.851 + !(this.hasBehavior("bookmark") || this.hasBehavior("tag"))) { 1.852 + showTags = false; 1.853 + match.style = "favicon"; 1.854 + } 1.855 + 1.856 + // If we have tags and should show them, we need to add them to the title. 1.857 + if (showTags) { 1.858 + title += TITLE_TAGS_SEPARATOR + tags; 1.859 + } 1.860 + 1.861 + // We have to determine the right style to display. Tags show the tag icon, 1.862 + // bookmarks get the bookmark icon, and keywords get the keyword icon. If 1.863 + // the result does not fall into any of those, it just gets the favicon. 1.864 + if (!match.style) { 1.865 + // It is possible that we already have a style set (from a keyword 1.866 + // search or because of the user's preferences), so only set it if we 1.867 + // haven't already done so. 1.868 + if (showTags) { 1.869 + match.style = "tag"; 1.870 + } 1.871 + else if (bookmarked) { 1.872 + match.style = "bookmark"; 1.873 + } 1.874 + } 1.875 + 1.876 + match.value = url; 1.877 + match.comment = title; 1.878 + if (iconurl) { 1.879 + match.icon = PlacesUtils.favicons 1.880 + .getFaviconLinkForIcon(NetUtil.newURI(iconurl)).spec; 1.881 + } 1.882 + match.frecency = frecency; 1.883 + 1.884 + return match; 1.885 + }, 1.886 + 1.887 + /** 1.888 + * Obtains the search query to be used based on the previously set search 1.889 + * behaviors (accessed by this.hasBehavior). 1.890 + * 1.891 + * @return an array consisting of the correctly optimized query to search the 1.892 + * database with and an object containing the params to bound. 1.893 + */ 1.894 + get _searchQuery() { 1.895 + // We use more optimized queries for restricted searches, so we will always 1.896 + // return the most restrictive one to the least restrictive one if more than 1.897 + // one token is found. 1.898 + // Note: "openpages" behavior is supported by the default query. 1.899 + // _switchToTabQuery instead returns only pages not supported by 1.900 + // history and it is always executed. 1.901 + let query = this.hasBehavior("tag") ? SQL_TAGS_QUERY : 1.902 + this.hasBehavior("bookmark") ? SQL_BOOKMARK_QUERY : 1.903 + this.hasBehavior("typed") ? SQL_TYPED_QUERY : 1.904 + this.hasBehavior("history") ? SQL_HISTORY_QUERY : 1.905 + SQL_DEFAULT_QUERY; 1.906 + 1.907 + return [ 1.908 + query, 1.909 + { 1.910 + parent: PlacesUtils.tagsFolderId, 1.911 + query_type: QUERYTYPE_FILTERED, 1.912 + matchBehavior: this._matchBehavior, 1.913 + searchBehavior: this._behavior, 1.914 + // We only want to search the tokens that we are left with - not the 1.915 + // original search string. 1.916 + searchString: this._searchTokens.join(" "), 1.917 + // Limit the query to the the maximum number of desired results. 1.918 + // This way we can avoid doing more work than needed. 1.919 + maxResults: Prefs.maxRichResults 1.920 + } 1.921 + ]; 1.922 + }, 1.923 + 1.924 + /** 1.925 + * Obtains the query to search for keywords. 1.926 + * 1.927 + * @return an array consisting of the correctly optimized query to search the 1.928 + * database with and an object containing the params to bound. 1.929 + */ 1.930 + get _keywordQuery() { 1.931 + // The keyword is the first word in the search string, with the parameters 1.932 + // following it. 1.933 + let searchString = this._originalSearchString; 1.934 + let queryString = ""; 1.935 + let queryIndex = searchString.indexOf(" "); 1.936 + if (queryIndex != -1) { 1.937 + queryString = searchString.substring(queryIndex + 1); 1.938 + } 1.939 + // We need to escape the parameters as if they were the query in a URL 1.940 + queryString = encodeURIComponent(queryString).replace("%20", "+", "g"); 1.941 + 1.942 + // The first word could be a keyword, so that's what we'll search. 1.943 + let keyword = this._searchTokens[0]; 1.944 + 1.945 + return [ 1.946 + SQL_KEYWORD_QUERY, 1.947 + { 1.948 + keyword: keyword, 1.949 + query_string: queryString, 1.950 + query_type: QUERYTYPE_KEYWORD 1.951 + } 1.952 + ]; 1.953 + }, 1.954 + 1.955 + /** 1.956 + * Obtains the query to search for switch-to-tab entries. 1.957 + * 1.958 + * @return an array consisting of the correctly optimized query to search the 1.959 + * database with and an object containing the params to bound. 1.960 + */ 1.961 + get _switchToTabQuery() [ 1.962 + SQL_SWITCHTAB_QUERY, 1.963 + { 1.964 + query_type: QUERYTYPE_FILTERED, 1.965 + matchBehavior: this._matchBehavior, 1.966 + searchBehavior: this._behavior, 1.967 + // We only want to search the tokens that we are left with - not the 1.968 + // original search string. 1.969 + searchString: this._searchTokens.join(" "), 1.970 + maxResults: Prefs.maxRichResults 1.971 + } 1.972 + ], 1.973 + 1.974 + /** 1.975 + * Obtains the query to search for adaptive results. 1.976 + * 1.977 + * @return an array consisting of the correctly optimized query to search the 1.978 + * database with and an object containing the params to bound. 1.979 + */ 1.980 + get _adaptiveQuery() [ 1.981 + SQL_ADAPTIVE_QUERY, 1.982 + { 1.983 + parent: PlacesUtils.tagsFolderId, 1.984 + search_string: this._searchString, 1.985 + query_type: QUERYTYPE_FILTERED, 1.986 + matchBehavior: this._matchBehavior, 1.987 + searchBehavior: this._behavior 1.988 + } 1.989 + ], 1.990 + 1.991 + /** 1.992 + * Whether we should try to autoFill. 1.993 + */ 1.994 + get _shouldAutofill() { 1.995 + // First of all, check for the autoFill pref. 1.996 + if (!Prefs.autofill) 1.997 + return false; 1.998 + 1.999 + // Then, we should not try to autofill if the behavior is not the default. 1.1000 + // TODO (bug 751709): Ideally we should have a more fine-grained behavior 1.1001 + // here, but for now it's enough to just check for default behavior. 1.1002 + if (Prefs.defaultBehavior != DEFAULT_BEHAVIOR) 1.1003 + return false; 1.1004 + 1.1005 + // Don't autoFill if the search term is recognized as a keyword, otherwise 1.1006 + // it will override default keywords behavior. Note that keywords are 1.1007 + // hashed on first use, so while the first query may delay a little bit, 1.1008 + // next ones will just hit the memory hash. 1.1009 + if (this._searchString.length == 0 || 1.1010 + PlacesUtils.bookmarks.getURIForKeyword(this._searchString)) { 1.1011 + return false; 1.1012 + } 1.1013 + 1.1014 + // Don't try to autofill if the search term includes any whitespace. 1.1015 + // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH 1.1016 + // tokenizer ends up trimming the search string and returning a value 1.1017 + // that doesn't match it, or is even shorter. 1.1018 + if (/\s/.test(this._searchString)) { 1.1019 + return false; 1.1020 + } 1.1021 + 1.1022 + return true; 1.1023 + }, 1.1024 + 1.1025 + /** 1.1026 + * Obtains the query to search for autoFill host results. 1.1027 + * 1.1028 + * @return an array consisting of the correctly optimized query to search the 1.1029 + * database with and an object containing the params to bound. 1.1030 + */ 1.1031 + get _hostQuery() [ 1.1032 + Prefs.autofillTyped ? SQL_TYPED_HOST_QUERY : SQL_TYPED_QUERY, 1.1033 + { 1.1034 + query_type: QUERYTYPE_AUTOFILL_HOST, 1.1035 + searchString: this._searchString.toLowerCase() 1.1036 + } 1.1037 + ], 1.1038 + 1.1039 + /** 1.1040 + * Obtains the query to search for autoFill url results. 1.1041 + * 1.1042 + * @return an array consisting of the correctly optimized query to search the 1.1043 + * database with and an object containing the params to bound. 1.1044 + */ 1.1045 + get _urlQuery() [ 1.1046 + Prefs.autofillTyped ? SQL_TYPED_HOST_QUERY : SQL_TYPED_QUERY, 1.1047 + { 1.1048 + query_type: QUERYTYPE_AUTOFILL_URL, 1.1049 + searchString: this._autofillUrlSearchString, 1.1050 + matchBehavior: MATCH_BEGINNING_CASE_SENSITIVE, 1.1051 + searchBehavior: Ci.mozIPlacesAutoComplete.BEHAVIOR_URL 1.1052 + } 1.1053 + ], 1.1054 + 1.1055 + /** 1.1056 + * Notifies the listener about results. 1.1057 + * 1.1058 + * @param searchOngoing 1.1059 + * Indicates whether the search is ongoing. 1.1060 + */ 1.1061 + notifyResults: function (searchOngoing) { 1.1062 + let result = this._result; 1.1063 + let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH"; 1.1064 + if (searchOngoing) { 1.1065 + resultCode += "_ONGOING"; 1.1066 + } 1.1067 + result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]); 1.1068 + this._listener.onSearchResult(this._autocompleteSearch, result); 1.1069 + }, 1.1070 +} 1.1071 + 1.1072 +//////////////////////////////////////////////////////////////////////////////// 1.1073 +//// UnifiedComplete class 1.1074 +//// component @mozilla.org/autocomplete/search;1?name=unifiedcomplete 1.1075 + 1.1076 +function UnifiedComplete() { 1.1077 + Services.obs.addObserver(this, TOPIC_SHUTDOWN, true); 1.1078 +} 1.1079 + 1.1080 +UnifiedComplete.prototype = { 1.1081 + ////////////////////////////////////////////////////////////////////////////// 1.1082 + //// nsIObserver 1.1083 + 1.1084 + observe: function (subject, topic, data) { 1.1085 + if (topic === TOPIC_SHUTDOWN) { 1.1086 + this.ensureShutdown(); 1.1087 + } 1.1088 + }, 1.1089 + 1.1090 + ////////////////////////////////////////////////////////////////////////////// 1.1091 + //// Database handling 1.1092 + 1.1093 + /** 1.1094 + * Promise resolved when the database initialization has completed, or null 1.1095 + * if it has never been requested. 1.1096 + */ 1.1097 + _promiseDatabase: null, 1.1098 + 1.1099 + /** 1.1100 + * Gets a Sqlite database handle. 1.1101 + * 1.1102 + * @return {Promise} 1.1103 + * @resolves to the Sqlite database handle (according to Sqlite.jsm). 1.1104 + * @rejects javascript exception. 1.1105 + */ 1.1106 + getDatabaseHandle: function () { 1.1107 + if (Prefs.enabled && !this._promiseDatabase) { 1.1108 + this._promiseDatabase = Task.spawn(function* () { 1.1109 + let conn = yield Sqlite.cloneStorageConnection({ 1.1110 + connection: PlacesUtils.history.DBConnection, 1.1111 + readOnly: true 1.1112 + }); 1.1113 + 1.1114 + // Autocomplete often fallbacks to a table scan due to lack of text 1.1115 + // indices. A larger cache helps reducing IO and improving performance. 1.1116 + // The value used here is larger than the default Storage value defined 1.1117 + // as MAX_CACHE_SIZE_BYTES in storage/src/mozStorageConnection.cpp. 1.1118 + yield conn.execute("PRAGMA cache_size = -6144"); // 6MiB 1.1119 + 1.1120 + yield SwitchToTabStorage.initDatabase(conn); 1.1121 + 1.1122 + return conn; 1.1123 + }.bind(this)).then(null, Cu.reportError); 1.1124 + } 1.1125 + return this._promiseDatabase; 1.1126 + }, 1.1127 + 1.1128 + /** 1.1129 + * Used to stop running queries and close the database handle. 1.1130 + */ 1.1131 + ensureShutdown: function () { 1.1132 + if (this._promiseDatabase) { 1.1133 + Task.spawn(function* () { 1.1134 + let conn = yield this.getDatabaseHandle(); 1.1135 + SwitchToTabStorage.shutdown(); 1.1136 + yield conn.close() 1.1137 + }.bind(this)).then(null, Cu.reportError); 1.1138 + this._promiseDatabase = null; 1.1139 + } 1.1140 + }, 1.1141 + 1.1142 + ////////////////////////////////////////////////////////////////////////////// 1.1143 + //// mozIPlacesAutoComplete 1.1144 + 1.1145 + registerOpenPage: function PAC_registerOpenPage(uri) { 1.1146 + SwitchToTabStorage.add(uri); 1.1147 + }, 1.1148 + 1.1149 + unregisterOpenPage: function PAC_unregisterOpenPage(uri) { 1.1150 + SwitchToTabStorage.delete(uri); 1.1151 + }, 1.1152 + 1.1153 + ////////////////////////////////////////////////////////////////////////////// 1.1154 + //// nsIAutoCompleteSearch 1.1155 + 1.1156 + startSearch: function (searchString, searchParam, previousResult, listener) { 1.1157 + // Stop the search in case the controller has not taken care of it. 1.1158 + if (this._currentSearch) { 1.1159 + this.stopSearch(); 1.1160 + } 1.1161 + 1.1162 + // Note: We don't use previousResult to make sure ordering of results are 1.1163 + // consistent. See bug 412730 for more details. 1.1164 + 1.1165 + this._currentSearch = new Search(searchString, searchParam, listener, 1.1166 + this, this); 1.1167 + 1.1168 + // If we are not enabled, we need to return now. Notice we need an empty 1.1169 + // result regardless, so we still create the Search object. 1.1170 + if (!Prefs.enabled) { 1.1171 + this.finishSearch(true); 1.1172 + return; 1.1173 + } 1.1174 + 1.1175 + let search = this._currentSearch; 1.1176 + this.getDatabaseHandle().then(conn => search.execute(conn)) 1.1177 + .then(() => { 1.1178 + if (search == this._currentSearch) { 1.1179 + this.finishSearch(true); 1.1180 + } 1.1181 + }, Cu.reportError); 1.1182 + }, 1.1183 + 1.1184 + stopSearch: function () { 1.1185 + if (this._currentSearch) { 1.1186 + this._currentSearch.cancel(); 1.1187 + } 1.1188 + this.finishSearch(); 1.1189 + }, 1.1190 + 1.1191 + /** 1.1192 + * Properly cleans up when searching is completed. 1.1193 + * 1.1194 + * @param notify [optional] 1.1195 + * Indicates if we should notify the AutoComplete listener about our 1.1196 + * results or not. 1.1197 + */ 1.1198 + finishSearch: function (notify=false) { 1.1199 + // Notify about results if we are supposed to. 1.1200 + if (notify) { 1.1201 + this._currentSearch.notifyResults(false); 1.1202 + } 1.1203 + 1.1204 + // Clear our state 1.1205 + TelemetryStopwatch.cancel(TELEMETRY_1ST_RESULT); 1.1206 + delete this._currentSearch; 1.1207 + }, 1.1208 + 1.1209 + ////////////////////////////////////////////////////////////////////////////// 1.1210 + //// nsIAutoCompleteSimpleResultListener 1.1211 + 1.1212 + onValueRemoved: function (result, spec, removeFromDB) { 1.1213 + if (removeFromDB) { 1.1214 + PlacesUtils.history.removePage(NetUtil.newURI(spec)); 1.1215 + } 1.1216 + }, 1.1217 + 1.1218 + ////////////////////////////////////////////////////////////////////////////// 1.1219 + //// nsIAutoCompleteSearchDescriptor 1.1220 + 1.1221 + get searchType() Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE, 1.1222 + 1.1223 + ////////////////////////////////////////////////////////////////////////////// 1.1224 + //// nsISupports 1.1225 + 1.1226 + classID: Components.ID("f964a319-397a-4d21-8be6-5cdd1ee3e3ae"), 1.1227 + 1.1228 + _xpcom_factory: XPCOMUtils.generateSingletonFactory(UnifiedComplete), 1.1229 + 1.1230 + QueryInterface: XPCOMUtils.generateQI([ 1.1231 + Ci.nsIAutoCompleteSearch, 1.1232 + Ci.nsIAutoCompleteSimpleResultListener, 1.1233 + Ci.mozIPlacesAutoComplete, 1.1234 + Ci.nsIObserver, 1.1235 + Ci.nsISupportsWeakReference 1.1236 + ]) 1.1237 +}; 1.1238 + 1.1239 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UnifiedComplete]);