Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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 | Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 8 | Components.utils.import("resource://gre/modules/Services.jsm"); |
michael@0 | 9 | XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", |
michael@0 | 10 | "resource://gre/modules/PlacesUtils.jsm"); |
michael@0 | 11 | XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", |
michael@0 | 12 | "resource://gre/modules/TelemetryStopwatch.jsm"); |
michael@0 | 13 | XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
michael@0 | 14 | "resource://gre/modules/NetUtil.jsm"); |
michael@0 | 15 | |
michael@0 | 16 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 17 | //// Constants |
michael@0 | 18 | |
michael@0 | 19 | const Cc = Components.classes; |
michael@0 | 20 | const Ci = Components.interfaces; |
michael@0 | 21 | const Cr = Components.results; |
michael@0 | 22 | |
michael@0 | 23 | // This SQL query fragment provides the following: |
michael@0 | 24 | // - whether the entry is bookmarked (kQueryIndexBookmarked) |
michael@0 | 25 | // - the bookmark title, if it is a bookmark (kQueryIndexBookmarkTitle) |
michael@0 | 26 | // - the tags associated with a bookmarked entry (kQueryIndexTags) |
michael@0 | 27 | const kBookTagSQLFragment = |
michael@0 | 28 | "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked, " |
michael@0 | 29 | + "( " |
michael@0 | 30 | + "SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL " |
michael@0 | 31 | + "ORDER BY lastModified DESC LIMIT 1 " |
michael@0 | 32 | + ") AS btitle, " |
michael@0 | 33 | + "( " |
michael@0 | 34 | + "SELECT GROUP_CONCAT(t.title, ',') " |
michael@0 | 35 | + "FROM moz_bookmarks b " |
michael@0 | 36 | + "JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent " |
michael@0 | 37 | + "WHERE b.fk = h.id " |
michael@0 | 38 | + ") AS tags"; |
michael@0 | 39 | |
michael@0 | 40 | // observer topics |
michael@0 | 41 | const kTopicShutdown = "places-shutdown"; |
michael@0 | 42 | const kPrefChanged = "nsPref:changed"; |
michael@0 | 43 | |
michael@0 | 44 | // Match type constants. These indicate what type of search function we should |
michael@0 | 45 | // be using. |
michael@0 | 46 | const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE; |
michael@0 | 47 | const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE; |
michael@0 | 48 | const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY; |
michael@0 | 49 | const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING; |
michael@0 | 50 | const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE; |
michael@0 | 51 | |
michael@0 | 52 | // AutoComplete index constants. All AutoComplete queries will provide these |
michael@0 | 53 | // columns in this order. |
michael@0 | 54 | const kQueryIndexURL = 0; |
michael@0 | 55 | const kQueryIndexTitle = 1; |
michael@0 | 56 | const kQueryIndexFaviconURL = 2; |
michael@0 | 57 | const kQueryIndexBookmarked = 3; |
michael@0 | 58 | const kQueryIndexBookmarkTitle = 4; |
michael@0 | 59 | const kQueryIndexTags = 5; |
michael@0 | 60 | const kQueryIndexVisitCount = 6; |
michael@0 | 61 | const kQueryIndexTyped = 7; |
michael@0 | 62 | const kQueryIndexPlaceId = 8; |
michael@0 | 63 | const kQueryIndexQueryType = 9; |
michael@0 | 64 | const kQueryIndexOpenPageCount = 10; |
michael@0 | 65 | |
michael@0 | 66 | // AutoComplete query type constants. Describes the various types of queries |
michael@0 | 67 | // that we can process. |
michael@0 | 68 | const kQueryTypeKeyword = 0; |
michael@0 | 69 | const kQueryTypeFiltered = 1; |
michael@0 | 70 | |
michael@0 | 71 | // This separator is used as an RTL-friendly way to split the title and tags. |
michael@0 | 72 | // It can also be used by an nsIAutoCompleteResult consumer to re-split the |
michael@0 | 73 | // "comment" back into the title and the tag. |
michael@0 | 74 | const kTitleTagsSeparator = " \u2013 "; |
michael@0 | 75 | |
michael@0 | 76 | const kBrowserUrlbarBranch = "browser.urlbar."; |
michael@0 | 77 | // Toggle autocomplete. |
michael@0 | 78 | const kBrowserUrlbarAutocompleteEnabledPref = "autocomplete.enabled"; |
michael@0 | 79 | // Toggle autoFill. |
michael@0 | 80 | const kBrowserUrlbarAutofillPref = "autoFill"; |
michael@0 | 81 | // Whether to search only typed entries. |
michael@0 | 82 | const kBrowserUrlbarAutofillTypedPref = "autoFill.typed"; |
michael@0 | 83 | |
michael@0 | 84 | // The Telemetry histogram for urlInlineComplete query on domain |
michael@0 | 85 | const DOMAIN_QUERY_TELEMETRY = "PLACES_AUTOCOMPLETE_URLINLINE_DOMAIN_QUERY_TIME_MS"; |
michael@0 | 86 | |
michael@0 | 87 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 88 | //// Globals |
michael@0 | 89 | |
michael@0 | 90 | XPCOMUtils.defineLazyServiceGetter(this, "gTextURIService", |
michael@0 | 91 | "@mozilla.org/intl/texttosuburi;1", |
michael@0 | 92 | "nsITextToSubURI"); |
michael@0 | 93 | |
michael@0 | 94 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 95 | //// Helpers |
michael@0 | 96 | |
michael@0 | 97 | /** |
michael@0 | 98 | * Initializes our temporary table on a given database. |
michael@0 | 99 | * |
michael@0 | 100 | * @param aDatabase |
michael@0 | 101 | * The mozIStorageConnection to set up the temp table on. |
michael@0 | 102 | */ |
michael@0 | 103 | function initTempTable(aDatabase) |
michael@0 | 104 | { |
michael@0 | 105 | // Note: this should be kept up-to-date with the definition in |
michael@0 | 106 | // nsPlacesTables.h. |
michael@0 | 107 | let stmt = aDatabase.createAsyncStatement( |
michael@0 | 108 | "CREATE TEMP TABLE moz_openpages_temp ( " |
michael@0 | 109 | + " url TEXT PRIMARY KEY " |
michael@0 | 110 | + ", open_count INTEGER " |
michael@0 | 111 | + ") " |
michael@0 | 112 | ); |
michael@0 | 113 | stmt.executeAsync(); |
michael@0 | 114 | stmt.finalize(); |
michael@0 | 115 | |
michael@0 | 116 | // Note: this should be kept up-to-date with the definition in |
michael@0 | 117 | // nsPlacesTriggers.h. |
michael@0 | 118 | stmt = aDatabase.createAsyncStatement( |
michael@0 | 119 | "CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger " |
michael@0 | 120 | + "AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW " |
michael@0 | 121 | + "WHEN NEW.open_count = 0 " |
michael@0 | 122 | + "BEGIN " |
michael@0 | 123 | + "DELETE FROM moz_openpages_temp " |
michael@0 | 124 | + "WHERE url = NEW.url; " |
michael@0 | 125 | + "END " |
michael@0 | 126 | ); |
michael@0 | 127 | stmt.executeAsync(); |
michael@0 | 128 | stmt.finalize(); |
michael@0 | 129 | } |
michael@0 | 130 | |
michael@0 | 131 | /** |
michael@0 | 132 | * Used to unescape encoded URI strings, and drop information that we do not |
michael@0 | 133 | * care about for searching. |
michael@0 | 134 | * |
michael@0 | 135 | * @param aURIString |
michael@0 | 136 | * The text to unescape and modify. |
michael@0 | 137 | * @return the modified uri. |
michael@0 | 138 | */ |
michael@0 | 139 | function fixupSearchText(aURIString) |
michael@0 | 140 | { |
michael@0 | 141 | let uri = stripPrefix(aURIString); |
michael@0 | 142 | return gTextURIService.unEscapeURIForUI("UTF-8", uri); |
michael@0 | 143 | } |
michael@0 | 144 | |
michael@0 | 145 | /** |
michael@0 | 146 | * Strip prefixes from the URI that we don't care about for searching. |
michael@0 | 147 | * |
michael@0 | 148 | * @param aURIString |
michael@0 | 149 | * The text to modify. |
michael@0 | 150 | * @return the modified uri. |
michael@0 | 151 | */ |
michael@0 | 152 | function stripPrefix(aURIString) |
michael@0 | 153 | { |
michael@0 | 154 | let uri = aURIString; |
michael@0 | 155 | |
michael@0 | 156 | if (uri.indexOf("http://") == 0) { |
michael@0 | 157 | uri = uri.slice(7); |
michael@0 | 158 | } |
michael@0 | 159 | else if (uri.indexOf("https://") == 0) { |
michael@0 | 160 | uri = uri.slice(8); |
michael@0 | 161 | } |
michael@0 | 162 | else if (uri.indexOf("ftp://") == 0) { |
michael@0 | 163 | uri = uri.slice(6); |
michael@0 | 164 | } |
michael@0 | 165 | |
michael@0 | 166 | if (uri.indexOf("www.") == 0) { |
michael@0 | 167 | uri = uri.slice(4); |
michael@0 | 168 | } |
michael@0 | 169 | return uri; |
michael@0 | 170 | } |
michael@0 | 171 | |
michael@0 | 172 | /** |
michael@0 | 173 | * safePrefGetter get the pref with typo safety. |
michael@0 | 174 | * This will return the default value provided if no pref is set. |
michael@0 | 175 | * |
michael@0 | 176 | * @param aPrefBranch |
michael@0 | 177 | * The nsIPrefBranch containing the required preference |
michael@0 | 178 | * @param aName |
michael@0 | 179 | * A preference name |
michael@0 | 180 | * @param aDefault |
michael@0 | 181 | * The preference's default value |
michael@0 | 182 | * @return the preference value or provided default |
michael@0 | 183 | */ |
michael@0 | 184 | |
michael@0 | 185 | function safePrefGetter(aPrefBranch, aName, aDefault) { |
michael@0 | 186 | let types = { |
michael@0 | 187 | boolean: "Bool", |
michael@0 | 188 | number: "Int", |
michael@0 | 189 | string: "Char" |
michael@0 | 190 | }; |
michael@0 | 191 | let type = types[typeof(aDefault)]; |
michael@0 | 192 | if (!type) { |
michael@0 | 193 | throw "Unknown type!"; |
michael@0 | 194 | } |
michael@0 | 195 | // If the pref isn't set, we want to use the default. |
michael@0 | 196 | try { |
michael@0 | 197 | return aPrefBranch["get" + type + "Pref"](aName); |
michael@0 | 198 | } |
michael@0 | 199 | catch (e) { |
michael@0 | 200 | return aDefault; |
michael@0 | 201 | } |
michael@0 | 202 | } |
michael@0 | 203 | |
michael@0 | 204 | |
michael@0 | 205 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 206 | //// AutoCompleteStatementCallbackWrapper class |
michael@0 | 207 | |
michael@0 | 208 | /** |
michael@0 | 209 | * Wraps a callback and ensures that handleCompletion is not dispatched if the |
michael@0 | 210 | * query is no longer tracked. |
michael@0 | 211 | * |
michael@0 | 212 | * @param aAutocomplete |
michael@0 | 213 | * A reference to a nsPlacesAutoComplete. |
michael@0 | 214 | * @param aCallback |
michael@0 | 215 | * A reference to a mozIStorageStatementCallback |
michael@0 | 216 | * @param aDBConnection |
michael@0 | 217 | * The database connection to execute the queries on. |
michael@0 | 218 | */ |
michael@0 | 219 | function AutoCompleteStatementCallbackWrapper(aAutocomplete, aCallback, |
michael@0 | 220 | aDBConnection) |
michael@0 | 221 | { |
michael@0 | 222 | this._autocomplete = aAutocomplete; |
michael@0 | 223 | this._callback = aCallback; |
michael@0 | 224 | this._db = aDBConnection; |
michael@0 | 225 | } |
michael@0 | 226 | |
michael@0 | 227 | AutoCompleteStatementCallbackWrapper.prototype = { |
michael@0 | 228 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 229 | //// mozIStorageStatementCallback |
michael@0 | 230 | |
michael@0 | 231 | handleResult: function ACSCW_handleResult(aResultSet) |
michael@0 | 232 | { |
michael@0 | 233 | this._callback.handleResult.apply(this._callback, arguments); |
michael@0 | 234 | }, |
michael@0 | 235 | |
michael@0 | 236 | handleError: function ACSCW_handleError(aError) |
michael@0 | 237 | { |
michael@0 | 238 | this._callback.handleError.apply(this._callback, arguments); |
michael@0 | 239 | }, |
michael@0 | 240 | |
michael@0 | 241 | handleCompletion: function ACSCW_handleCompletion(aReason) |
michael@0 | 242 | { |
michael@0 | 243 | // Only dispatch handleCompletion if we are not done searching and are a |
michael@0 | 244 | // pending search. |
michael@0 | 245 | if (!this._autocomplete.isSearchComplete() && |
michael@0 | 246 | this._autocomplete.isPendingSearch(this._handle)) { |
michael@0 | 247 | this._callback.handleCompletion.apply(this._callback, arguments); |
michael@0 | 248 | } |
michael@0 | 249 | }, |
michael@0 | 250 | |
michael@0 | 251 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 252 | //// AutoCompleteStatementCallbackWrapper |
michael@0 | 253 | |
michael@0 | 254 | /** |
michael@0 | 255 | * Executes the specified query asynchronously. This object will notify |
michael@0 | 256 | * this._callback if we should notify (logic explained in handleCompletion). |
michael@0 | 257 | * |
michael@0 | 258 | * @param aQueries |
michael@0 | 259 | * The queries to execute asynchronously. |
michael@0 | 260 | * @return a mozIStoragePendingStatement that can be used to cancel the |
michael@0 | 261 | * queries. |
michael@0 | 262 | */ |
michael@0 | 263 | executeAsync: function ACSCW_executeAsync(aQueries) |
michael@0 | 264 | { |
michael@0 | 265 | return this._handle = this._db.executeAsync(aQueries, aQueries.length, |
michael@0 | 266 | this); |
michael@0 | 267 | }, |
michael@0 | 268 | |
michael@0 | 269 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 270 | //// nsISupports |
michael@0 | 271 | |
michael@0 | 272 | QueryInterface: XPCOMUtils.generateQI([ |
michael@0 | 273 | Ci.mozIStorageStatementCallback, |
michael@0 | 274 | ]) |
michael@0 | 275 | }; |
michael@0 | 276 | |
michael@0 | 277 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 278 | //// nsPlacesAutoComplete class |
michael@0 | 279 | //// @mozilla.org/autocomplete/search;1?name=history |
michael@0 | 280 | |
michael@0 | 281 | function nsPlacesAutoComplete() |
michael@0 | 282 | { |
michael@0 | 283 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 284 | //// Shared Constants for Smart Getters |
michael@0 | 285 | |
michael@0 | 286 | // TODO bug 412736 in case of a frecency tie, break it with h.typed and |
michael@0 | 287 | // h.visit_count which is better than nothing. This is slow, so not doing it |
michael@0 | 288 | // yet... |
michael@0 | 289 | const SQL_BASE = "SELECT h.url, h.title, f.url, " + kBookTagSQLFragment + ", " |
michael@0 | 290 | + "h.visit_count, h.typed, h.id, :query_type, " |
michael@0 | 291 | + "t.open_count " |
michael@0 | 292 | + "FROM moz_places h " |
michael@0 | 293 | + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id " |
michael@0 | 294 | + "LEFT JOIN moz_openpages_temp t ON t.url = h.url " |
michael@0 | 295 | + "WHERE h.frecency <> 0 " |
michael@0 | 296 | + "AND AUTOCOMPLETE_MATCH(:searchString, h.url, " |
michael@0 | 297 | + "IFNULL(btitle, h.title), tags, " |
michael@0 | 298 | + "h.visit_count, h.typed, " |
michael@0 | 299 | + "bookmarked, t.open_count, " |
michael@0 | 300 | + ":matchBehavior, :searchBehavior) " |
michael@0 | 301 | + "{ADDITIONAL_CONDITIONS} " |
michael@0 | 302 | + "ORDER BY h.frecency DESC, h.id DESC " |
michael@0 | 303 | + "LIMIT :maxResults"; |
michael@0 | 304 | |
michael@0 | 305 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 306 | //// Smart Getters |
michael@0 | 307 | |
michael@0 | 308 | XPCOMUtils.defineLazyGetter(this, "_db", function() { |
michael@0 | 309 | // Get a cloned, read-only version of the database. We'll only ever write |
michael@0 | 310 | // to our own in-memory temp table, and having a cloned copy means we do not |
michael@0 | 311 | // run the risk of our queries taking longer due to the main database |
michael@0 | 312 | // connection performing a long-running task. |
michael@0 | 313 | let db = PlacesUtils.history.DBConnection.clone(true); |
michael@0 | 314 | |
michael@0 | 315 | // Autocomplete often fallbacks to a table scan due to lack of text indices. |
michael@0 | 316 | // In such cases a larger cache helps reducing IO. The default Storage |
michael@0 | 317 | // value is MAX_CACHE_SIZE_BYTES in storage/src/mozStorageConnection.cpp. |
michael@0 | 318 | let stmt = db.createAsyncStatement("PRAGMA cache_size = -6144"); // 6MiB |
michael@0 | 319 | stmt.executeAsync(); |
michael@0 | 320 | stmt.finalize(); |
michael@0 | 321 | |
michael@0 | 322 | // Create our in-memory tables for tab tracking. |
michael@0 | 323 | initTempTable(db); |
michael@0 | 324 | |
michael@0 | 325 | // Populate the table with current open pages cache contents. |
michael@0 | 326 | if (this._openPagesCache.length > 0) { |
michael@0 | 327 | // Avoid getter re-entrance from the _registerOpenPageQuery lazy getter. |
michael@0 | 328 | let stmt = this._registerOpenPageQuery = |
michael@0 | 329 | db.createAsyncStatement(this._registerOpenPageQuerySQL); |
michael@0 | 330 | let params = stmt.newBindingParamsArray(); |
michael@0 | 331 | for (let i = 0; i < this._openPagesCache.length; i++) { |
michael@0 | 332 | let bp = params.newBindingParams(); |
michael@0 | 333 | bp.bindByName("page_url", this._openPagesCache[i]); |
michael@0 | 334 | params.addParams(bp); |
michael@0 | 335 | } |
michael@0 | 336 | stmt.bindParameters(params); |
michael@0 | 337 | stmt.executeAsync(); |
michael@0 | 338 | stmt.finalize(); |
michael@0 | 339 | delete this._openPagesCache; |
michael@0 | 340 | } |
michael@0 | 341 | |
michael@0 | 342 | return db; |
michael@0 | 343 | }); |
michael@0 | 344 | |
michael@0 | 345 | XPCOMUtils.defineLazyGetter(this, "_defaultQuery", function() { |
michael@0 | 346 | let replacementText = ""; |
michael@0 | 347 | return this._db.createAsyncStatement( |
michael@0 | 348 | SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g") |
michael@0 | 349 | ); |
michael@0 | 350 | }); |
michael@0 | 351 | |
michael@0 | 352 | XPCOMUtils.defineLazyGetter(this, "_historyQuery", function() { |
michael@0 | 353 | // Enforce ignoring the visit_count index, since the frecency one is much |
michael@0 | 354 | // faster in this case. ANALYZE helps the query planner to figure out the |
michael@0 | 355 | // faster path, but it may not have run yet. |
michael@0 | 356 | let replacementText = "AND +h.visit_count > 0"; |
michael@0 | 357 | return this._db.createAsyncStatement( |
michael@0 | 358 | SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g") |
michael@0 | 359 | ); |
michael@0 | 360 | }); |
michael@0 | 361 | |
michael@0 | 362 | XPCOMUtils.defineLazyGetter(this, "_bookmarkQuery", function() { |
michael@0 | 363 | let replacementText = "AND bookmarked"; |
michael@0 | 364 | return this._db.createAsyncStatement( |
michael@0 | 365 | SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g") |
michael@0 | 366 | ); |
michael@0 | 367 | }); |
michael@0 | 368 | |
michael@0 | 369 | XPCOMUtils.defineLazyGetter(this, "_tagsQuery", function() { |
michael@0 | 370 | let replacementText = "AND tags IS NOT NULL"; |
michael@0 | 371 | return this._db.createAsyncStatement( |
michael@0 | 372 | SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g") |
michael@0 | 373 | ); |
michael@0 | 374 | }); |
michael@0 | 375 | |
michael@0 | 376 | XPCOMUtils.defineLazyGetter(this, "_openPagesQuery", function() { |
michael@0 | 377 | return this._db.createAsyncStatement( |
michael@0 | 378 | "SELECT t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL, " |
michael@0 | 379 | + ":query_type, t.open_count, NULL " |
michael@0 | 380 | + "FROM moz_openpages_temp t " |
michael@0 | 381 | + "LEFT JOIN moz_places h ON h.url = t.url " |
michael@0 | 382 | + "WHERE h.id IS NULL " |
michael@0 | 383 | + "AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL, " |
michael@0 | 384 | + "NULL, NULL, NULL, t.open_count, " |
michael@0 | 385 | + ":matchBehavior, :searchBehavior) " |
michael@0 | 386 | + "ORDER BY t.ROWID DESC " |
michael@0 | 387 | + "LIMIT :maxResults " |
michael@0 | 388 | ); |
michael@0 | 389 | }); |
michael@0 | 390 | |
michael@0 | 391 | XPCOMUtils.defineLazyGetter(this, "_typedQuery", function() { |
michael@0 | 392 | let replacementText = "AND h.typed = 1"; |
michael@0 | 393 | return this._db.createAsyncStatement( |
michael@0 | 394 | SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g") |
michael@0 | 395 | ); |
michael@0 | 396 | }); |
michael@0 | 397 | |
michael@0 | 398 | XPCOMUtils.defineLazyGetter(this, "_adaptiveQuery", function() { |
michael@0 | 399 | return this._db.createAsyncStatement( |
michael@0 | 400 | "/* do not warn (bug 487789) */ " |
michael@0 | 401 | + "SELECT h.url, h.title, f.url, " + kBookTagSQLFragment + ", " |
michael@0 | 402 | + "h.visit_count, h.typed, h.id, :query_type, t.open_count " |
michael@0 | 403 | + "FROM ( " |
michael@0 | 404 | + "SELECT ROUND( " |
michael@0 | 405 | + "MAX(use_count) * (1 + (input = :search_string)), 1 " |
michael@0 | 406 | + ") AS rank, place_id " |
michael@0 | 407 | + "FROM moz_inputhistory " |
michael@0 | 408 | + "WHERE input BETWEEN :search_string AND :search_string || X'FFFF' " |
michael@0 | 409 | + "GROUP BY place_id " |
michael@0 | 410 | + ") AS i " |
michael@0 | 411 | + "JOIN moz_places h ON h.id = i.place_id " |
michael@0 | 412 | + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id " |
michael@0 | 413 | + "LEFT JOIN moz_openpages_temp t ON t.url = h.url " |
michael@0 | 414 | + "WHERE AUTOCOMPLETE_MATCH(NULL, h.url, " |
michael@0 | 415 | + "IFNULL(btitle, h.title), tags, " |
michael@0 | 416 | + "h.visit_count, h.typed, bookmarked, " |
michael@0 | 417 | + "t.open_count, " |
michael@0 | 418 | + ":matchBehavior, :searchBehavior) " |
michael@0 | 419 | + "ORDER BY rank DESC, h.frecency DESC " |
michael@0 | 420 | ); |
michael@0 | 421 | }); |
michael@0 | 422 | |
michael@0 | 423 | XPCOMUtils.defineLazyGetter(this, "_keywordQuery", function() { |
michael@0 | 424 | return this._db.createAsyncStatement( |
michael@0 | 425 | "/* do not warn (bug 487787) */ " |
michael@0 | 426 | + "SELECT " |
michael@0 | 427 | + "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk) " |
michael@0 | 428 | + "AS search_url, h.title, " |
michael@0 | 429 | + "IFNULL(f.url, (SELECT f.url " |
michael@0 | 430 | + "FROM moz_places " |
michael@0 | 431 | + "JOIN moz_favicons f ON f.id = favicon_id " |
michael@0 | 432 | + "WHERE rev_host = (SELECT rev_host FROM moz_places WHERE id = b.fk) " |
michael@0 | 433 | + "ORDER BY frecency DESC " |
michael@0 | 434 | + "LIMIT 1) " |
michael@0 | 435 | + "), 1, b.title, NULL, h.visit_count, h.typed, IFNULL(h.id, b.fk), " |
michael@0 | 436 | + ":query_type, t.open_count " |
michael@0 | 437 | + "FROM moz_keywords k " |
michael@0 | 438 | + "JOIN moz_bookmarks b ON b.keyword_id = k.id " |
michael@0 | 439 | + "LEFT JOIN moz_places h ON h.url = search_url " |
michael@0 | 440 | + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id " |
michael@0 | 441 | + "LEFT JOIN moz_openpages_temp t ON t.url = search_url " |
michael@0 | 442 | + "WHERE LOWER(k.keyword) = LOWER(:keyword) " |
michael@0 | 443 | + "ORDER BY h.frecency DESC " |
michael@0 | 444 | ); |
michael@0 | 445 | }); |
michael@0 | 446 | |
michael@0 | 447 | this._registerOpenPageQuerySQL = "INSERT OR REPLACE INTO moz_openpages_temp " |
michael@0 | 448 | + "(url, open_count) " |
michael@0 | 449 | + "VALUES (:page_url, " |
michael@0 | 450 | + "IFNULL(" |
michael@0 | 451 | + "(" |
michael@0 | 452 | + "SELECT open_count + 1 " |
michael@0 | 453 | + "FROM moz_openpages_temp " |
michael@0 | 454 | + "WHERE url = :page_url " |
michael@0 | 455 | + "), " |
michael@0 | 456 | + "1" |
michael@0 | 457 | + ")" |
michael@0 | 458 | + ")"; |
michael@0 | 459 | XPCOMUtils.defineLazyGetter(this, "_registerOpenPageQuery", function() { |
michael@0 | 460 | return this._db.createAsyncStatement(this._registerOpenPageQuerySQL); |
michael@0 | 461 | }); |
michael@0 | 462 | |
michael@0 | 463 | XPCOMUtils.defineLazyGetter(this, "_unregisterOpenPageQuery", function() { |
michael@0 | 464 | return this._db.createAsyncStatement( |
michael@0 | 465 | "UPDATE moz_openpages_temp " |
michael@0 | 466 | + "SET open_count = open_count - 1 " |
michael@0 | 467 | + "WHERE url = :page_url" |
michael@0 | 468 | ); |
michael@0 | 469 | }); |
michael@0 | 470 | |
michael@0 | 471 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 472 | //// Initialization |
michael@0 | 473 | |
michael@0 | 474 | // load preferences |
michael@0 | 475 | this._prefs = Cc["@mozilla.org/preferences-service;1"]. |
michael@0 | 476 | getService(Ci.nsIPrefService). |
michael@0 | 477 | getBranch(kBrowserUrlbarBranch); |
michael@0 | 478 | this._loadPrefs(true); |
michael@0 | 479 | |
michael@0 | 480 | // register observers |
michael@0 | 481 | this._os = Cc["@mozilla.org/observer-service;1"]. |
michael@0 | 482 | getService(Ci.nsIObserverService); |
michael@0 | 483 | this._os.addObserver(this, kTopicShutdown, false); |
michael@0 | 484 | |
michael@0 | 485 | } |
michael@0 | 486 | |
michael@0 | 487 | nsPlacesAutoComplete.prototype = { |
michael@0 | 488 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 489 | //// nsIAutoCompleteSearch |
michael@0 | 490 | |
michael@0 | 491 | startSearch: function PAC_startSearch(aSearchString, aSearchParam, |
michael@0 | 492 | aPreviousResult, aListener) |
michael@0 | 493 | { |
michael@0 | 494 | // Stop the search in case the controller has not taken care of it. |
michael@0 | 495 | this.stopSearch(); |
michael@0 | 496 | |
michael@0 | 497 | // Note: We don't use aPreviousResult to make sure ordering of results are |
michael@0 | 498 | // consistent. See bug 412730 for more details. |
michael@0 | 499 | |
michael@0 | 500 | // We want to store the original string with no leading or trailing |
michael@0 | 501 | // whitespace for case sensitive searches. |
michael@0 | 502 | this._originalSearchString = aSearchString.trim(); |
michael@0 | 503 | |
michael@0 | 504 | this._currentSearchString = |
michael@0 | 505 | fixupSearchText(this._originalSearchString.toLowerCase()); |
michael@0 | 506 | |
michael@0 | 507 | let searchParamParts = aSearchParam.split(" "); |
michael@0 | 508 | this._enableActions = searchParamParts.indexOf("enable-actions") != -1; |
michael@0 | 509 | |
michael@0 | 510 | this._listener = aListener; |
michael@0 | 511 | let result = Cc["@mozilla.org/autocomplete/simple-result;1"]. |
michael@0 | 512 | createInstance(Ci.nsIAutoCompleteSimpleResult); |
michael@0 | 513 | result.setSearchString(aSearchString); |
michael@0 | 514 | result.setListener(this); |
michael@0 | 515 | this._result = result; |
michael@0 | 516 | |
michael@0 | 517 | // If we are not enabled, we need to return now. |
michael@0 | 518 | if (!this._enabled) { |
michael@0 | 519 | this._finishSearch(true); |
michael@0 | 520 | return; |
michael@0 | 521 | } |
michael@0 | 522 | |
michael@0 | 523 | // Reset our search behavior to the default. |
michael@0 | 524 | if (this._currentSearchString) { |
michael@0 | 525 | this._behavior = this._defaultBehavior; |
michael@0 | 526 | } |
michael@0 | 527 | else { |
michael@0 | 528 | this._behavior = this._emptySearchDefaultBehavior; |
michael@0 | 529 | } |
michael@0 | 530 | // For any given search, we run up to four queries: |
michael@0 | 531 | // 1) keywords (this._keywordQuery) |
michael@0 | 532 | // 2) adaptive learning (this._adaptiveQuery) |
michael@0 | 533 | // 3) open pages not supported by history (this._openPagesQuery) |
michael@0 | 534 | // 4) query from this._getSearch |
michael@0 | 535 | // (1) only gets ran if we get any filtered tokens from this._getSearch, |
michael@0 | 536 | // since if there are no tokens, there is nothing to match, so there is no |
michael@0 | 537 | // reason to run the query). |
michael@0 | 538 | let {query, tokens} = |
michael@0 | 539 | this._getSearch(this._getUnfilteredSearchTokens(this._currentSearchString)); |
michael@0 | 540 | let queries = tokens.length ? |
michael@0 | 541 | [this._getBoundKeywordQuery(tokens), this._getBoundAdaptiveQuery(), this._getBoundOpenPagesQuery(tokens), query] : |
michael@0 | 542 | [this._getBoundAdaptiveQuery(), this._getBoundOpenPagesQuery(tokens), query]; |
michael@0 | 543 | |
michael@0 | 544 | // Start executing our queries. |
michael@0 | 545 | this._telemetryStartTime = Date.now(); |
michael@0 | 546 | this._executeQueries(queries); |
michael@0 | 547 | |
michael@0 | 548 | // Set up our persistent state for the duration of the search. |
michael@0 | 549 | this._searchTokens = tokens; |
michael@0 | 550 | this._usedPlaces = {}; |
michael@0 | 551 | }, |
michael@0 | 552 | |
michael@0 | 553 | stopSearch: function PAC_stopSearch() |
michael@0 | 554 | { |
michael@0 | 555 | // We need to cancel our searches so we do not get any [more] results. |
michael@0 | 556 | // However, it's possible we haven't actually started any searches, so this |
michael@0 | 557 | // method may throw because this._pendingQuery may be undefined. |
michael@0 | 558 | if (this._pendingQuery) { |
michael@0 | 559 | this._stopActiveQuery(); |
michael@0 | 560 | } |
michael@0 | 561 | |
michael@0 | 562 | this._finishSearch(false); |
michael@0 | 563 | }, |
michael@0 | 564 | |
michael@0 | 565 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 566 | //// nsIAutoCompleteSimpleResultListener |
michael@0 | 567 | |
michael@0 | 568 | onValueRemoved: function PAC_onValueRemoved(aResult, aURISpec, aRemoveFromDB) |
michael@0 | 569 | { |
michael@0 | 570 | if (aRemoveFromDB) { |
michael@0 | 571 | PlacesUtils.history.removePage(NetUtil.newURI(aURISpec)); |
michael@0 | 572 | } |
michael@0 | 573 | }, |
michael@0 | 574 | |
michael@0 | 575 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 576 | //// mozIPlacesAutoComplete |
michael@0 | 577 | |
michael@0 | 578 | // If the connection has not yet been started, use this local cache. This |
michael@0 | 579 | // prevents autocomplete from initing the database till the first search. |
michael@0 | 580 | _openPagesCache: [], |
michael@0 | 581 | registerOpenPage: function PAC_registerOpenPage(aURI) |
michael@0 | 582 | { |
michael@0 | 583 | if (!this._databaseInitialized) { |
michael@0 | 584 | this._openPagesCache.push(aURI.spec); |
michael@0 | 585 | return; |
michael@0 | 586 | } |
michael@0 | 587 | |
michael@0 | 588 | let stmt = this._registerOpenPageQuery; |
michael@0 | 589 | stmt.params.page_url = aURI.spec; |
michael@0 | 590 | stmt.executeAsync(); |
michael@0 | 591 | }, |
michael@0 | 592 | |
michael@0 | 593 | unregisterOpenPage: function PAC_unregisterOpenPage(aURI) |
michael@0 | 594 | { |
michael@0 | 595 | if (!this._databaseInitialized) { |
michael@0 | 596 | let index = this._openPagesCache.indexOf(aURI.spec); |
michael@0 | 597 | if (index != -1) { |
michael@0 | 598 | this._openPagesCache.splice(index, 1); |
michael@0 | 599 | } |
michael@0 | 600 | return; |
michael@0 | 601 | } |
michael@0 | 602 | |
michael@0 | 603 | let stmt = this._unregisterOpenPageQuery; |
michael@0 | 604 | stmt.params.page_url = aURI.spec; |
michael@0 | 605 | stmt.executeAsync(); |
michael@0 | 606 | }, |
michael@0 | 607 | |
michael@0 | 608 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 609 | //// mozIStorageStatementCallback |
michael@0 | 610 | |
michael@0 | 611 | handleResult: function PAC_handleResult(aResultSet) |
michael@0 | 612 | { |
michael@0 | 613 | let row, haveMatches = false; |
michael@0 | 614 | while ((row = aResultSet.getNextRow())) { |
michael@0 | 615 | let match = this._processRow(row); |
michael@0 | 616 | haveMatches = haveMatches || match; |
michael@0 | 617 | |
michael@0 | 618 | if (this._result.matchCount == this._maxRichResults) { |
michael@0 | 619 | // We have enough results, so stop running our search. |
michael@0 | 620 | this._stopActiveQuery(); |
michael@0 | 621 | |
michael@0 | 622 | // And finish our search. |
michael@0 | 623 | this._finishSearch(true); |
michael@0 | 624 | return; |
michael@0 | 625 | } |
michael@0 | 626 | |
michael@0 | 627 | } |
michael@0 | 628 | |
michael@0 | 629 | // Notify about results if we've gotten them. |
michael@0 | 630 | if (haveMatches) { |
michael@0 | 631 | this._notifyResults(true); |
michael@0 | 632 | } |
michael@0 | 633 | }, |
michael@0 | 634 | |
michael@0 | 635 | handleError: function PAC_handleError(aError) |
michael@0 | 636 | { |
michael@0 | 637 | Components.utils.reportError("Places AutoComplete: An async statement encountered an " + |
michael@0 | 638 | "error: " + aError.result + ", '" + aError.message + "'"); |
michael@0 | 639 | }, |
michael@0 | 640 | |
michael@0 | 641 | handleCompletion: function PAC_handleCompletion(aReason) |
michael@0 | 642 | { |
michael@0 | 643 | // If we have already finished our search, we should bail out early. |
michael@0 | 644 | if (this.isSearchComplete()) { |
michael@0 | 645 | return; |
michael@0 | 646 | } |
michael@0 | 647 | |
michael@0 | 648 | // If we do not have enough results, and our match type is |
michael@0 | 649 | // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more |
michael@0 | 650 | // results. |
michael@0 | 651 | if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE && |
michael@0 | 652 | this._result.matchCount < this._maxRichResults && !this._secondPass) { |
michael@0 | 653 | this._secondPass = true; |
michael@0 | 654 | let queries = [ |
michael@0 | 655 | this._getBoundAdaptiveQuery(MATCH_ANYWHERE), |
michael@0 | 656 | this._getBoundSearchQuery(MATCH_ANYWHERE, this._searchTokens), |
michael@0 | 657 | ]; |
michael@0 | 658 | this._executeQueries(queries); |
michael@0 | 659 | return; |
michael@0 | 660 | } |
michael@0 | 661 | |
michael@0 | 662 | this._finishSearch(true); |
michael@0 | 663 | }, |
michael@0 | 664 | |
michael@0 | 665 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 666 | //// nsIObserver |
michael@0 | 667 | |
michael@0 | 668 | observe: function PAC_observe(aSubject, aTopic, aData) |
michael@0 | 669 | { |
michael@0 | 670 | if (aTopic == kTopicShutdown) { |
michael@0 | 671 | this._os.removeObserver(this, kTopicShutdown); |
michael@0 | 672 | |
michael@0 | 673 | // Remove our preference observer. |
michael@0 | 674 | this._prefs.removeObserver("", this); |
michael@0 | 675 | delete this._prefs; |
michael@0 | 676 | |
michael@0 | 677 | // Finalize the statements that we have used. |
michael@0 | 678 | let stmts = [ |
michael@0 | 679 | "_defaultQuery", |
michael@0 | 680 | "_historyQuery", |
michael@0 | 681 | "_bookmarkQuery", |
michael@0 | 682 | "_tagsQuery", |
michael@0 | 683 | "_openPagesQuery", |
michael@0 | 684 | "_typedQuery", |
michael@0 | 685 | "_adaptiveQuery", |
michael@0 | 686 | "_keywordQuery", |
michael@0 | 687 | "_registerOpenPageQuery", |
michael@0 | 688 | "_unregisterOpenPageQuery", |
michael@0 | 689 | ]; |
michael@0 | 690 | for (let i = 0; i < stmts.length; i++) { |
michael@0 | 691 | // We do not want to create any query we haven't already created, so |
michael@0 | 692 | // see if it is a getter first. |
michael@0 | 693 | if (Object.getOwnPropertyDescriptor(this, stmts[i]).value !== undefined) { |
michael@0 | 694 | this[stmts[i]].finalize(); |
michael@0 | 695 | } |
michael@0 | 696 | } |
michael@0 | 697 | |
michael@0 | 698 | if (this._databaseInitialized) { |
michael@0 | 699 | this._db.asyncClose(); |
michael@0 | 700 | } |
michael@0 | 701 | } |
michael@0 | 702 | else if (aTopic == kPrefChanged) { |
michael@0 | 703 | this._loadPrefs(); |
michael@0 | 704 | } |
michael@0 | 705 | }, |
michael@0 | 706 | |
michael@0 | 707 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 708 | //// nsPlacesAutoComplete |
michael@0 | 709 | |
michael@0 | 710 | get _databaseInitialized() |
michael@0 | 711 | Object.getOwnPropertyDescriptor(this, "_db").value !== undefined, |
michael@0 | 712 | |
michael@0 | 713 | /** |
michael@0 | 714 | * Generates the tokens used in searching from a given string. |
michael@0 | 715 | * |
michael@0 | 716 | * @param aSearchString |
michael@0 | 717 | * The string to generate tokens from. |
michael@0 | 718 | * @return an array of tokens. |
michael@0 | 719 | */ |
michael@0 | 720 | _getUnfilteredSearchTokens: function PAC_unfilteredSearchTokens(aSearchString) |
michael@0 | 721 | { |
michael@0 | 722 | // Calling split on an empty string will return an array containing one |
michael@0 | 723 | // empty string. We don't want that, as it'll break our logic, so return an |
michael@0 | 724 | // empty array then. |
michael@0 | 725 | return aSearchString.length ? aSearchString.split(" ") : []; |
michael@0 | 726 | }, |
michael@0 | 727 | |
michael@0 | 728 | /** |
michael@0 | 729 | * Properly cleans up when searching is completed. |
michael@0 | 730 | * |
michael@0 | 731 | * @param aNotify |
michael@0 | 732 | * Indicates if we should notify the AutoComplete listener about our |
michael@0 | 733 | * results or not. |
michael@0 | 734 | */ |
michael@0 | 735 | _finishSearch: function PAC_finishSearch(aNotify) |
michael@0 | 736 | { |
michael@0 | 737 | // Notify about results if we are supposed to. |
michael@0 | 738 | if (aNotify) { |
michael@0 | 739 | this._notifyResults(false); |
michael@0 | 740 | } |
michael@0 | 741 | |
michael@0 | 742 | // Clear our state |
michael@0 | 743 | delete this._originalSearchString; |
michael@0 | 744 | delete this._currentSearchString; |
michael@0 | 745 | delete this._strippedPrefix; |
michael@0 | 746 | delete this._searchTokens; |
michael@0 | 747 | delete this._listener; |
michael@0 | 748 | delete this._result; |
michael@0 | 749 | delete this._usedPlaces; |
michael@0 | 750 | delete this._pendingQuery; |
michael@0 | 751 | this._secondPass = false; |
michael@0 | 752 | this._enableActions = false; |
michael@0 | 753 | }, |
michael@0 | 754 | |
michael@0 | 755 | /** |
michael@0 | 756 | * Executes the given queries asynchronously. |
michael@0 | 757 | * |
michael@0 | 758 | * @param aQueries |
michael@0 | 759 | * The queries to execute. |
michael@0 | 760 | */ |
michael@0 | 761 | _executeQueries: function PAC_executeQueries(aQueries) |
michael@0 | 762 | { |
michael@0 | 763 | // Because we might get a handleCompletion for canceled queries, we want to |
michael@0 | 764 | // filter out queries we no longer care about (described in the |
michael@0 | 765 | // handleCompletion implementation of AutoCompleteStatementCallbackWrapper). |
michael@0 | 766 | |
michael@0 | 767 | // Create our wrapper object and execute the queries. |
michael@0 | 768 | let wrapper = new AutoCompleteStatementCallbackWrapper(this, this, this._db); |
michael@0 | 769 | this._pendingQuery = wrapper.executeAsync(aQueries); |
michael@0 | 770 | }, |
michael@0 | 771 | |
michael@0 | 772 | /** |
michael@0 | 773 | * Stops executing our active query. |
michael@0 | 774 | */ |
michael@0 | 775 | _stopActiveQuery: function PAC_stopActiveQuery() |
michael@0 | 776 | { |
michael@0 | 777 | this._pendingQuery.cancel(); |
michael@0 | 778 | delete this._pendingQuery; |
michael@0 | 779 | }, |
michael@0 | 780 | |
michael@0 | 781 | /** |
michael@0 | 782 | * Notifies the listener about results. |
michael@0 | 783 | * |
michael@0 | 784 | * @param aSearchOngoing |
michael@0 | 785 | * Indicates if the search is ongoing or not. |
michael@0 | 786 | */ |
michael@0 | 787 | _notifyResults: function PAC_notifyResults(aSearchOngoing) |
michael@0 | 788 | { |
michael@0 | 789 | let result = this._result; |
michael@0 | 790 | let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH"; |
michael@0 | 791 | if (aSearchOngoing) { |
michael@0 | 792 | resultCode += "_ONGOING"; |
michael@0 | 793 | } |
michael@0 | 794 | result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]); |
michael@0 | 795 | this._listener.onSearchResult(this, result); |
michael@0 | 796 | if (this._telemetryStartTime) { |
michael@0 | 797 | let elapsed = Date.now() - this._telemetryStartTime; |
michael@0 | 798 | if (elapsed > 50) { |
michael@0 | 799 | try { |
michael@0 | 800 | Services.telemetry |
michael@0 | 801 | .getHistogramById("PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS") |
michael@0 | 802 | .add(elapsed); |
michael@0 | 803 | } catch (ex) { |
michael@0 | 804 | Components.utils.reportError("Unable to report telemetry."); |
michael@0 | 805 | } |
michael@0 | 806 | } |
michael@0 | 807 | this._telemetryStartTime = null; |
michael@0 | 808 | } |
michael@0 | 809 | }, |
michael@0 | 810 | |
michael@0 | 811 | /** |
michael@0 | 812 | * Loads the preferences that we care about. |
michael@0 | 813 | * |
michael@0 | 814 | * @param [optional] aRegisterObserver |
michael@0 | 815 | * Indicates if the preference observer should be added or not. The |
michael@0 | 816 | * default value is false. |
michael@0 | 817 | */ |
michael@0 | 818 | _loadPrefs: function PAC_loadPrefs(aRegisterObserver) |
michael@0 | 819 | { |
michael@0 | 820 | this._enabled = safePrefGetter(this._prefs, |
michael@0 | 821 | kBrowserUrlbarAutocompleteEnabledPref, |
michael@0 | 822 | true); |
michael@0 | 823 | this._matchBehavior = safePrefGetter(this._prefs, |
michael@0 | 824 | "matchBehavior", |
michael@0 | 825 | MATCH_BOUNDARY_ANYWHERE); |
michael@0 | 826 | this._filterJavaScript = safePrefGetter(this._prefs, "filter.javascript", true); |
michael@0 | 827 | this._maxRichResults = safePrefGetter(this._prefs, "maxRichResults", 25); |
michael@0 | 828 | this._restrictHistoryToken = safePrefGetter(this._prefs, |
michael@0 | 829 | "restrict.history", "^"); |
michael@0 | 830 | this._restrictBookmarkToken = safePrefGetter(this._prefs, |
michael@0 | 831 | "restrict.bookmark", "*"); |
michael@0 | 832 | this._restrictTypedToken = safePrefGetter(this._prefs, "restrict.typed", "~"); |
michael@0 | 833 | this._restrictTagToken = safePrefGetter(this._prefs, "restrict.tag", "+"); |
michael@0 | 834 | this._restrictOpenPageToken = safePrefGetter(this._prefs, |
michael@0 | 835 | "restrict.openpage", "%"); |
michael@0 | 836 | this._matchTitleToken = safePrefGetter(this._prefs, "match.title", "#"); |
michael@0 | 837 | this._matchURLToken = safePrefGetter(this._prefs, "match.url", "@"); |
michael@0 | 838 | this._defaultBehavior = safePrefGetter(this._prefs, "default.behavior", 0); |
michael@0 | 839 | // Further restrictions to apply for "empty searches" (i.e. searches for ""). |
michael@0 | 840 | this._emptySearchDefaultBehavior = |
michael@0 | 841 | this._defaultBehavior | |
michael@0 | 842 | safePrefGetter(this._prefs, "default.behavior.emptyRestriction", |
michael@0 | 843 | Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY | |
michael@0 | 844 | Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED); |
michael@0 | 845 | |
michael@0 | 846 | // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE. |
michael@0 | 847 | if (this._matchBehavior != MATCH_ANYWHERE && |
michael@0 | 848 | this._matchBehavior != MATCH_BOUNDARY && |
michael@0 | 849 | this._matchBehavior != MATCH_BEGINNING) { |
michael@0 | 850 | this._matchBehavior = MATCH_BOUNDARY_ANYWHERE; |
michael@0 | 851 | } |
michael@0 | 852 | // register observer |
michael@0 | 853 | if (aRegisterObserver) { |
michael@0 | 854 | this._prefs.addObserver("", this, false); |
michael@0 | 855 | } |
michael@0 | 856 | }, |
michael@0 | 857 | |
michael@0 | 858 | /** |
michael@0 | 859 | * Given an array of tokens, this function determines which query should be |
michael@0 | 860 | * ran. It also removes any special search tokens. |
michael@0 | 861 | * |
michael@0 | 862 | * @param aTokens |
michael@0 | 863 | * An array of search tokens. |
michael@0 | 864 | * @return an object with two properties: |
michael@0 | 865 | * query: the correctly optimized, bound query to search the database |
michael@0 | 866 | * with. |
michael@0 | 867 | * tokens: the filtered list of tokens to search with. |
michael@0 | 868 | */ |
michael@0 | 869 | _getSearch: function PAC_getSearch(aTokens) |
michael@0 | 870 | { |
michael@0 | 871 | // Set the proper behavior so our call to _getBoundSearchQuery gives us the |
michael@0 | 872 | // correct query. |
michael@0 | 873 | for (let i = aTokens.length - 1; i >= 0; i--) { |
michael@0 | 874 | switch (aTokens[i]) { |
michael@0 | 875 | case this._restrictHistoryToken: |
michael@0 | 876 | this._setBehavior("history"); |
michael@0 | 877 | break; |
michael@0 | 878 | case this._restrictBookmarkToken: |
michael@0 | 879 | this._setBehavior("bookmark"); |
michael@0 | 880 | break; |
michael@0 | 881 | case this._restrictTagToken: |
michael@0 | 882 | this._setBehavior("tag"); |
michael@0 | 883 | break; |
michael@0 | 884 | case this._restrictOpenPageToken: |
michael@0 | 885 | if (!this._enableActions) { |
michael@0 | 886 | continue; |
michael@0 | 887 | } |
michael@0 | 888 | this._setBehavior("openpage"); |
michael@0 | 889 | break; |
michael@0 | 890 | case this._matchTitleToken: |
michael@0 | 891 | this._setBehavior("title"); |
michael@0 | 892 | break; |
michael@0 | 893 | case this._matchURLToken: |
michael@0 | 894 | this._setBehavior("url"); |
michael@0 | 895 | break; |
michael@0 | 896 | case this._restrictTypedToken: |
michael@0 | 897 | this._setBehavior("typed"); |
michael@0 | 898 | break; |
michael@0 | 899 | default: |
michael@0 | 900 | // We do not want to remove the token if we did not match. |
michael@0 | 901 | continue; |
michael@0 | 902 | }; |
michael@0 | 903 | |
michael@0 | 904 | aTokens.splice(i, 1); |
michael@0 | 905 | } |
michael@0 | 906 | |
michael@0 | 907 | // Set the right JavaScript behavior based on our preference. Note that the |
michael@0 | 908 | // preference is whether or not we should filter JavaScript, and the |
michael@0 | 909 | // behavior is if we should search it or not. |
michael@0 | 910 | if (!this._filterJavaScript) { |
michael@0 | 911 | this._setBehavior("javascript"); |
michael@0 | 912 | } |
michael@0 | 913 | |
michael@0 | 914 | return { |
michael@0 | 915 | query: this._getBoundSearchQuery(this._matchBehavior, aTokens), |
michael@0 | 916 | tokens: aTokens |
michael@0 | 917 | }; |
michael@0 | 918 | }, |
michael@0 | 919 | |
michael@0 | 920 | /** |
michael@0 | 921 | * Obtains the search query to be used based on the previously set search |
michael@0 | 922 | * behaviors (accessed by this._hasBehavior). The query is bound and ready to |
michael@0 | 923 | * execute. |
michael@0 | 924 | * |
michael@0 | 925 | * @param aMatchBehavior |
michael@0 | 926 | * How this query should match its tokens to the search string. |
michael@0 | 927 | * @param aTokens |
michael@0 | 928 | * An array of search tokens. |
michael@0 | 929 | * @return the correctly optimized query to search the database with and the |
michael@0 | 930 | * new list of tokens to search with. The query has all the needed |
michael@0 | 931 | * parameters bound, so consumers can execute it without doing any |
michael@0 | 932 | * additional work. |
michael@0 | 933 | */ |
michael@0 | 934 | _getBoundSearchQuery: function PAC_getBoundSearchQuery(aMatchBehavior, |
michael@0 | 935 | aTokens) |
michael@0 | 936 | { |
michael@0 | 937 | // We use more optimized queries for restricted searches, so we will always |
michael@0 | 938 | // return the most restrictive one to the least restrictive one if more than |
michael@0 | 939 | // one token is found. |
michael@0 | 940 | // Note: "openpages" behavior is supported by the default query. |
michael@0 | 941 | // _openPagesQuery instead returns only pages not supported by |
michael@0 | 942 | // history and it is always executed. |
michael@0 | 943 | let query = this._hasBehavior("tag") ? this._tagsQuery : |
michael@0 | 944 | this._hasBehavior("bookmark") ? this._bookmarkQuery : |
michael@0 | 945 | this._hasBehavior("typed") ? this._typedQuery : |
michael@0 | 946 | this._hasBehavior("history") ? this._historyQuery : |
michael@0 | 947 | this._defaultQuery; |
michael@0 | 948 | |
michael@0 | 949 | // Bind the needed parameters to the query so consumers can use it. |
michael@0 | 950 | let (params = query.params) { |
michael@0 | 951 | params.parent = PlacesUtils.tagsFolderId; |
michael@0 | 952 | params.query_type = kQueryTypeFiltered; |
michael@0 | 953 | params.matchBehavior = aMatchBehavior; |
michael@0 | 954 | params.searchBehavior = this._behavior; |
michael@0 | 955 | |
michael@0 | 956 | // We only want to search the tokens that we are left with - not the |
michael@0 | 957 | // original search string. |
michael@0 | 958 | params.searchString = aTokens.join(" "); |
michael@0 | 959 | |
michael@0 | 960 | // Limit the query to the the maximum number of desired results. |
michael@0 | 961 | // This way we can avoid doing more work than needed. |
michael@0 | 962 | params.maxResults = this._maxRichResults; |
michael@0 | 963 | } |
michael@0 | 964 | |
michael@0 | 965 | return query; |
michael@0 | 966 | }, |
michael@0 | 967 | |
michael@0 | 968 | _getBoundOpenPagesQuery: function PAC_getBoundOpenPagesQuery(aTokens) |
michael@0 | 969 | { |
michael@0 | 970 | let query = this._openPagesQuery; |
michael@0 | 971 | |
michael@0 | 972 | // Bind the needed parameters to the query so consumers can use it. |
michael@0 | 973 | let (params = query.params) { |
michael@0 | 974 | params.query_type = kQueryTypeFiltered; |
michael@0 | 975 | params.matchBehavior = this._matchBehavior; |
michael@0 | 976 | params.searchBehavior = this._behavior; |
michael@0 | 977 | // We only want to search the tokens that we are left with - not the |
michael@0 | 978 | // original search string. |
michael@0 | 979 | params.searchString = aTokens.join(" "); |
michael@0 | 980 | params.maxResults = this._maxRichResults; |
michael@0 | 981 | } |
michael@0 | 982 | |
michael@0 | 983 | return query; |
michael@0 | 984 | }, |
michael@0 | 985 | |
michael@0 | 986 | /** |
michael@0 | 987 | * Obtains the keyword query with the properly bound parameters. |
michael@0 | 988 | * |
michael@0 | 989 | * @param aTokens |
michael@0 | 990 | * The array of search tokens to check against. |
michael@0 | 991 | * @return the bound keyword query. |
michael@0 | 992 | */ |
michael@0 | 993 | _getBoundKeywordQuery: function PAC_getBoundKeywordQuery(aTokens) |
michael@0 | 994 | { |
michael@0 | 995 | // The keyword is the first word in the search string, with the parameters |
michael@0 | 996 | // following it. |
michael@0 | 997 | let searchString = this._originalSearchString; |
michael@0 | 998 | let queryString = ""; |
michael@0 | 999 | let queryIndex = searchString.indexOf(" "); |
michael@0 | 1000 | if (queryIndex != -1) { |
michael@0 | 1001 | queryString = searchString.substring(queryIndex + 1); |
michael@0 | 1002 | } |
michael@0 | 1003 | // We need to escape the parameters as if they were the query in a URL |
michael@0 | 1004 | queryString = encodeURIComponent(queryString).replace("%20", "+", "g"); |
michael@0 | 1005 | |
michael@0 | 1006 | // The first word could be a keyword, so that's what we'll search. |
michael@0 | 1007 | let keyword = aTokens[0]; |
michael@0 | 1008 | |
michael@0 | 1009 | let query = this._keywordQuery; |
michael@0 | 1010 | let (params = query.params) { |
michael@0 | 1011 | params.keyword = keyword; |
michael@0 | 1012 | params.query_string = queryString; |
michael@0 | 1013 | params.query_type = kQueryTypeKeyword; |
michael@0 | 1014 | } |
michael@0 | 1015 | |
michael@0 | 1016 | return query; |
michael@0 | 1017 | }, |
michael@0 | 1018 | |
michael@0 | 1019 | /** |
michael@0 | 1020 | * Obtains the adaptive query with the properly bound parameters. |
michael@0 | 1021 | * |
michael@0 | 1022 | * @return the bound adaptive query. |
michael@0 | 1023 | */ |
michael@0 | 1024 | _getBoundAdaptiveQuery: function PAC_getBoundAdaptiveQuery(aMatchBehavior) |
michael@0 | 1025 | { |
michael@0 | 1026 | // If we were not given a match behavior, use the stored match behavior. |
michael@0 | 1027 | if (arguments.length == 0) { |
michael@0 | 1028 | aMatchBehavior = this._matchBehavior; |
michael@0 | 1029 | } |
michael@0 | 1030 | |
michael@0 | 1031 | let query = this._adaptiveQuery; |
michael@0 | 1032 | let (params = query.params) { |
michael@0 | 1033 | params.parent = PlacesUtils.tagsFolderId; |
michael@0 | 1034 | params.search_string = this._currentSearchString; |
michael@0 | 1035 | params.query_type = kQueryTypeFiltered; |
michael@0 | 1036 | params.matchBehavior = aMatchBehavior; |
michael@0 | 1037 | params.searchBehavior = this._behavior; |
michael@0 | 1038 | } |
michael@0 | 1039 | |
michael@0 | 1040 | return query; |
michael@0 | 1041 | }, |
michael@0 | 1042 | |
michael@0 | 1043 | /** |
michael@0 | 1044 | * Processes a mozIStorageRow to generate the proper data for the AutoComplete |
michael@0 | 1045 | * result. This will add an entry to the current result if it matches the |
michael@0 | 1046 | * criteria. |
michael@0 | 1047 | * |
michael@0 | 1048 | * @param aRow |
michael@0 | 1049 | * The row to process. |
michael@0 | 1050 | * @return true if the row is accepted, and false if not. |
michael@0 | 1051 | */ |
michael@0 | 1052 | _processRow: function PAC_processRow(aRow) |
michael@0 | 1053 | { |
michael@0 | 1054 | // Before we do any work, make sure this entry isn't already in our results. |
michael@0 | 1055 | let entryId = aRow.getResultByIndex(kQueryIndexPlaceId); |
michael@0 | 1056 | let escapedEntryURL = aRow.getResultByIndex(kQueryIndexURL); |
michael@0 | 1057 | let openPageCount = aRow.getResultByIndex(kQueryIndexOpenPageCount) || 0; |
michael@0 | 1058 | |
michael@0 | 1059 | // If actions are enabled and the page is open, add only the switch-to-tab |
michael@0 | 1060 | // result. Otherwise, add the normal result. |
michael@0 | 1061 | let [url, action] = this._enableActions && openPageCount > 0 ? |
michael@0 | 1062 | ["moz-action:switchtab," + escapedEntryURL, "action "] : |
michael@0 | 1063 | [escapedEntryURL, ""]; |
michael@0 | 1064 | |
michael@0 | 1065 | if (this._inResults(entryId, url)) { |
michael@0 | 1066 | return false; |
michael@0 | 1067 | } |
michael@0 | 1068 | |
michael@0 | 1069 | let entryTitle = aRow.getResultByIndex(kQueryIndexTitle) || ""; |
michael@0 | 1070 | let entryFavicon = aRow.getResultByIndex(kQueryIndexFaviconURL) || ""; |
michael@0 | 1071 | let entryBookmarked = aRow.getResultByIndex(kQueryIndexBookmarked); |
michael@0 | 1072 | let entryBookmarkTitle = entryBookmarked ? |
michael@0 | 1073 | aRow.getResultByIndex(kQueryIndexBookmarkTitle) : null; |
michael@0 | 1074 | let entryTags = aRow.getResultByIndex(kQueryIndexTags) || ""; |
michael@0 | 1075 | |
michael@0 | 1076 | // Always prefer the bookmark title unless it is empty |
michael@0 | 1077 | let title = entryBookmarkTitle || entryTitle; |
michael@0 | 1078 | |
michael@0 | 1079 | let style; |
michael@0 | 1080 | if (aRow.getResultByIndex(kQueryIndexQueryType) == kQueryTypeKeyword) { |
michael@0 | 1081 | // If we do not have a title, then we must have a keyword, so let the UI |
michael@0 | 1082 | // know it is a keyword. Otherwise, we found an exact page match, so just |
michael@0 | 1083 | // show the page like a regular result. Because the page title is likely |
michael@0 | 1084 | // going to be more specific than the bookmark title (keyword title). |
michael@0 | 1085 | if (!entryTitle) { |
michael@0 | 1086 | style = "keyword"; |
michael@0 | 1087 | } |
michael@0 | 1088 | else { |
michael@0 | 1089 | title = entryTitle; |
michael@0 | 1090 | } |
michael@0 | 1091 | } |
michael@0 | 1092 | |
michael@0 | 1093 | // We will always prefer to show tags if we have them. |
michael@0 | 1094 | let showTags = !!entryTags; |
michael@0 | 1095 | |
michael@0 | 1096 | // However, we'll act as if a page is not bookmarked or tagged if the user |
michael@0 | 1097 | // only wants only history and not bookmarks or tags. |
michael@0 | 1098 | if (this._hasBehavior("history") && |
michael@0 | 1099 | !(this._hasBehavior("bookmark") || this._hasBehavior("tag"))) { |
michael@0 | 1100 | showTags = false; |
michael@0 | 1101 | style = "favicon"; |
michael@0 | 1102 | } |
michael@0 | 1103 | |
michael@0 | 1104 | // If we have tags and should show them, we need to add them to the title. |
michael@0 | 1105 | if (showTags) { |
michael@0 | 1106 | title += kTitleTagsSeparator + entryTags; |
michael@0 | 1107 | } |
michael@0 | 1108 | // We have to determine the right style to display. Tags show the tag icon, |
michael@0 | 1109 | // bookmarks get the bookmark icon, and keywords get the keyword icon. If |
michael@0 | 1110 | // the result does not fall into any of those, it just gets the favicon. |
michael@0 | 1111 | if (!style) { |
michael@0 | 1112 | // It is possible that we already have a style set (from a keyword |
michael@0 | 1113 | // search or because of the user's preferences), so only set it if we |
michael@0 | 1114 | // haven't already done so. |
michael@0 | 1115 | if (showTags) { |
michael@0 | 1116 | style = "tag"; |
michael@0 | 1117 | } |
michael@0 | 1118 | else if (entryBookmarked) { |
michael@0 | 1119 | style = "bookmark"; |
michael@0 | 1120 | } |
michael@0 | 1121 | else { |
michael@0 | 1122 | style = "favicon"; |
michael@0 | 1123 | } |
michael@0 | 1124 | } |
michael@0 | 1125 | |
michael@0 | 1126 | this._addToResults(entryId, url, title, entryFavicon, action + style); |
michael@0 | 1127 | return true; |
michael@0 | 1128 | }, |
michael@0 | 1129 | |
michael@0 | 1130 | /** |
michael@0 | 1131 | * Checks to see if the given place has already been added to the results. |
michael@0 | 1132 | * |
michael@0 | 1133 | * @param aPlaceId |
michael@0 | 1134 | * The place id to check for, may be null. |
michael@0 | 1135 | * @param aUrl |
michael@0 | 1136 | * The url to check for. |
michael@0 | 1137 | * @return true if the place has been added, false otherwise. |
michael@0 | 1138 | * |
michael@0 | 1139 | * @note Must check both the id and the url for a negative match, since |
michael@0 | 1140 | * autocomplete may run in the middle of a new page addition. In such |
michael@0 | 1141 | * a case the switch-to-tab query would hash the page by url, then a |
michael@0 | 1142 | * next query, running after the page addition, would hash it by id. |
michael@0 | 1143 | * It's not possible to just rely on url though, since keywords |
michael@0 | 1144 | * dynamically modify the url to include their search string. |
michael@0 | 1145 | */ |
michael@0 | 1146 | _inResults: function PAC_inResults(aPlaceId, aUrl) |
michael@0 | 1147 | { |
michael@0 | 1148 | if (aPlaceId && aPlaceId in this._usedPlaces) { |
michael@0 | 1149 | return true; |
michael@0 | 1150 | } |
michael@0 | 1151 | return aUrl in this._usedPlaces; |
michael@0 | 1152 | }, |
michael@0 | 1153 | |
michael@0 | 1154 | /** |
michael@0 | 1155 | * Adds a result to the AutoComplete results. Also tracks that we've added |
michael@0 | 1156 | * this place_id into the result set. |
michael@0 | 1157 | * |
michael@0 | 1158 | * @param aPlaceId |
michael@0 | 1159 | * The place_id of the item to be added to the result set. This is |
michael@0 | 1160 | * used by _inResults. |
michael@0 | 1161 | * @param aURISpec |
michael@0 | 1162 | * The URI spec for the entry. |
michael@0 | 1163 | * @param aTitle |
michael@0 | 1164 | * The title to give the entry. |
michael@0 | 1165 | * @param aFaviconSpec |
michael@0 | 1166 | * The favicon to give to the entry. |
michael@0 | 1167 | * @param aStyle |
michael@0 | 1168 | * Indicates how the entry should be styled when displayed. |
michael@0 | 1169 | */ |
michael@0 | 1170 | _addToResults: function PAC_addToResults(aPlaceId, aURISpec, aTitle, |
michael@0 | 1171 | aFaviconSpec, aStyle) |
michael@0 | 1172 | { |
michael@0 | 1173 | // Add this to our internal tracker to ensure duplicates do not end up in |
michael@0 | 1174 | // the result. _usedPlaces is an Object that is being used as a set. |
michael@0 | 1175 | // Not all entries have a place id, thus we fallback to the url for them. |
michael@0 | 1176 | // We cannot use only the url since keywords entries are modified to |
michael@0 | 1177 | // include the search string, and would be returned multiple times. Ids |
michael@0 | 1178 | // are faster too. |
michael@0 | 1179 | this._usedPlaces[aPlaceId || aURISpec] = true; |
michael@0 | 1180 | |
michael@0 | 1181 | // Obtain the favicon for this URI. |
michael@0 | 1182 | let favicon; |
michael@0 | 1183 | if (aFaviconSpec) { |
michael@0 | 1184 | let uri = NetUtil.newURI(aFaviconSpec); |
michael@0 | 1185 | favicon = PlacesUtils.favicons.getFaviconLinkForIcon(uri).spec; |
michael@0 | 1186 | } |
michael@0 | 1187 | favicon = favicon || PlacesUtils.favicons.defaultFavicon.spec; |
michael@0 | 1188 | |
michael@0 | 1189 | this._result.appendMatch(aURISpec, aTitle, favicon, aStyle); |
michael@0 | 1190 | }, |
michael@0 | 1191 | |
michael@0 | 1192 | /** |
michael@0 | 1193 | * Determines if the specified AutoComplete behavior is set. |
michael@0 | 1194 | * |
michael@0 | 1195 | * @param aType |
michael@0 | 1196 | * The behavior type to test for. |
michael@0 | 1197 | * @return true if the behavior is set, false otherwise. |
michael@0 | 1198 | */ |
michael@0 | 1199 | _hasBehavior: function PAC_hasBehavior(aType) |
michael@0 | 1200 | { |
michael@0 | 1201 | return (this._behavior & |
michael@0 | 1202 | Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()]); |
michael@0 | 1203 | }, |
michael@0 | 1204 | |
michael@0 | 1205 | /** |
michael@0 | 1206 | * Enables the desired AutoComplete behavior. |
michael@0 | 1207 | * |
michael@0 | 1208 | * @param aType |
michael@0 | 1209 | * The behavior type to set. |
michael@0 | 1210 | */ |
michael@0 | 1211 | _setBehavior: function PAC_setBehavior(aType) |
michael@0 | 1212 | { |
michael@0 | 1213 | this._behavior |= |
michael@0 | 1214 | Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()]; |
michael@0 | 1215 | }, |
michael@0 | 1216 | |
michael@0 | 1217 | /** |
michael@0 | 1218 | * Determines if we are done searching or not. |
michael@0 | 1219 | * |
michael@0 | 1220 | * @return true if we have completed searching, false otherwise. |
michael@0 | 1221 | */ |
michael@0 | 1222 | isSearchComplete: function PAC_isSearchComplete() |
michael@0 | 1223 | { |
michael@0 | 1224 | // If _pendingQuery is null, we should no longer do any work since we have |
michael@0 | 1225 | // already called _finishSearch. This means we completed our search. |
michael@0 | 1226 | return this._pendingQuery == null; |
michael@0 | 1227 | }, |
michael@0 | 1228 | |
michael@0 | 1229 | /** |
michael@0 | 1230 | * Determines if the given handle of a pending statement is a pending search |
michael@0 | 1231 | * or not. |
michael@0 | 1232 | * |
michael@0 | 1233 | * @param aHandle |
michael@0 | 1234 | * A mozIStoragePendingStatement to check and see if we are waiting for |
michael@0 | 1235 | * results from it still. |
michael@0 | 1236 | * @return true if it is a pending query, false otherwise. |
michael@0 | 1237 | */ |
michael@0 | 1238 | isPendingSearch: function PAC_isPendingSearch(aHandle) |
michael@0 | 1239 | { |
michael@0 | 1240 | return this._pendingQuery == aHandle; |
michael@0 | 1241 | }, |
michael@0 | 1242 | |
michael@0 | 1243 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1244 | //// nsISupports |
michael@0 | 1245 | |
michael@0 | 1246 | classID: Components.ID("d0272978-beab-4adc-a3d4-04b76acfa4e7"), |
michael@0 | 1247 | |
michael@0 | 1248 | _xpcom_factory: XPCOMUtils.generateSingletonFactory(nsPlacesAutoComplete), |
michael@0 | 1249 | |
michael@0 | 1250 | QueryInterface: XPCOMUtils.generateQI([ |
michael@0 | 1251 | Ci.nsIAutoCompleteSearch, |
michael@0 | 1252 | Ci.nsIAutoCompleteSimpleResultListener, |
michael@0 | 1253 | Ci.mozIPlacesAutoComplete, |
michael@0 | 1254 | Ci.mozIStorageStatementCallback, |
michael@0 | 1255 | Ci.nsIObserver, |
michael@0 | 1256 | Ci.nsISupportsWeakReference, |
michael@0 | 1257 | ]) |
michael@0 | 1258 | }; |
michael@0 | 1259 | |
michael@0 | 1260 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1261 | //// urlInlineComplete class |
michael@0 | 1262 | //// component @mozilla.org/autocomplete/search;1?name=urlinline |
michael@0 | 1263 | |
michael@0 | 1264 | function urlInlineComplete() |
michael@0 | 1265 | { |
michael@0 | 1266 | this._loadPrefs(true); |
michael@0 | 1267 | Services.obs.addObserver(this, kTopicShutdown, true); |
michael@0 | 1268 | } |
michael@0 | 1269 | |
michael@0 | 1270 | urlInlineComplete.prototype = { |
michael@0 | 1271 | |
michael@0 | 1272 | ///////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1273 | //// Database and query getters |
michael@0 | 1274 | |
michael@0 | 1275 | __db: null, |
michael@0 | 1276 | |
michael@0 | 1277 | get _db() |
michael@0 | 1278 | { |
michael@0 | 1279 | if (!this.__db && this._autofillEnabled) { |
michael@0 | 1280 | this.__db = PlacesUtils.history.DBConnection.clone(true); |
michael@0 | 1281 | } |
michael@0 | 1282 | return this.__db; |
michael@0 | 1283 | }, |
michael@0 | 1284 | |
michael@0 | 1285 | __hostQuery: null, |
michael@0 | 1286 | |
michael@0 | 1287 | get _hostQuery() |
michael@0 | 1288 | { |
michael@0 | 1289 | if (!this.__hostQuery) { |
michael@0 | 1290 | // Add a trailing slash at the end of the hostname, since we always |
michael@0 | 1291 | // want to complete up to and including a URL separator. |
michael@0 | 1292 | this.__hostQuery = this._db.createAsyncStatement( |
michael@0 | 1293 | "/* do not warn (bug no): could index on (typed,frecency) but not worth it */ " |
michael@0 | 1294 | + "SELECT host || '/', prefix || host || '/' " |
michael@0 | 1295 | + "FROM moz_hosts " |
michael@0 | 1296 | + "WHERE host BETWEEN :search_string AND :search_string || X'FFFF' " |
michael@0 | 1297 | + "AND frecency <> 0 " |
michael@0 | 1298 | + (this._autofillTyped ? "AND typed = 1 " : "") |
michael@0 | 1299 | + "ORDER BY frecency DESC " |
michael@0 | 1300 | + "LIMIT 1" |
michael@0 | 1301 | ); |
michael@0 | 1302 | } |
michael@0 | 1303 | return this.__hostQuery; |
michael@0 | 1304 | }, |
michael@0 | 1305 | |
michael@0 | 1306 | __urlQuery: null, |
michael@0 | 1307 | |
michael@0 | 1308 | get _urlQuery() |
michael@0 | 1309 | { |
michael@0 | 1310 | if (!this.__urlQuery) { |
michael@0 | 1311 | this.__urlQuery = this._db.createAsyncStatement( |
michael@0 | 1312 | "/* do not warn (bug no): can't use an index */ " |
michael@0 | 1313 | + "SELECT h.url " |
michael@0 | 1314 | + "FROM moz_places h " |
michael@0 | 1315 | + "WHERE h.frecency <> 0 " |
michael@0 | 1316 | + (this._autofillTyped ? "AND h.typed = 1 " : "") |
michael@0 | 1317 | + "AND AUTOCOMPLETE_MATCH(:searchString, h.url, " |
michael@0 | 1318 | + "h.title, '', " |
michael@0 | 1319 | + "h.visit_count, h.typed, 0, 0, " |
michael@0 | 1320 | + ":matchBehavior, :searchBehavior) " |
michael@0 | 1321 | + "ORDER BY h.frecency DESC, h.id DESC " |
michael@0 | 1322 | + "LIMIT 1" |
michael@0 | 1323 | ); |
michael@0 | 1324 | } |
michael@0 | 1325 | return this.__urlQuery; |
michael@0 | 1326 | }, |
michael@0 | 1327 | |
michael@0 | 1328 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1329 | //// nsIAutoCompleteSearch |
michael@0 | 1330 | |
michael@0 | 1331 | startSearch: function UIC_startSearch(aSearchString, aSearchParam, |
michael@0 | 1332 | aPreviousResult, aListener) |
michael@0 | 1333 | { |
michael@0 | 1334 | // Stop the search in case the controller has not taken care of it. |
michael@0 | 1335 | if (this._pendingQuery) { |
michael@0 | 1336 | this.stopSearch(); |
michael@0 | 1337 | } |
michael@0 | 1338 | |
michael@0 | 1339 | // We want to store the original string with no leading or trailing |
michael@0 | 1340 | // whitespace for case sensitive searches. |
michael@0 | 1341 | this._originalSearchString = aSearchString; |
michael@0 | 1342 | this._currentSearchString = |
michael@0 | 1343 | fixupSearchText(this._originalSearchString.toLowerCase()); |
michael@0 | 1344 | // The protocol and the host are lowercased by nsIURI, so it's fine to |
michael@0 | 1345 | // lowercase the typed prefix to add it back to the results later. |
michael@0 | 1346 | this._strippedPrefix = this._originalSearchString.slice( |
michael@0 | 1347 | 0, this._originalSearchString.length - this._currentSearchString.length |
michael@0 | 1348 | ).toLowerCase(); |
michael@0 | 1349 | |
michael@0 | 1350 | this._result = Cc["@mozilla.org/autocomplete/simple-result;1"]. |
michael@0 | 1351 | createInstance(Ci.nsIAutoCompleteSimpleResult); |
michael@0 | 1352 | this._result.setSearchString(aSearchString); |
michael@0 | 1353 | this._result.setTypeAheadResult(true); |
michael@0 | 1354 | |
michael@0 | 1355 | this._listener = aListener; |
michael@0 | 1356 | |
michael@0 | 1357 | // Don't autoFill if the search term is recognized as a keyword, otherwise |
michael@0 | 1358 | // it will override default keywords behavior. Note that keywords are |
michael@0 | 1359 | // hashed on first use, so while the first query may delay a little bit, |
michael@0 | 1360 | // next ones will just hit the memory hash. |
michael@0 | 1361 | if (this._currentSearchString.length == 0 || !this._db || |
michael@0 | 1362 | PlacesUtils.bookmarks.getURIForKeyword(this._currentSearchString)) { |
michael@0 | 1363 | this._finishSearch(); |
michael@0 | 1364 | return; |
michael@0 | 1365 | } |
michael@0 | 1366 | |
michael@0 | 1367 | // Don't try to autofill if the search term includes any whitespace. |
michael@0 | 1368 | // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH |
michael@0 | 1369 | // tokenizer ends up trimming the search string and returning a value |
michael@0 | 1370 | // that doesn't match it, or is even shorter. |
michael@0 | 1371 | if (/\s/.test(this._currentSearchString)) { |
michael@0 | 1372 | this._finishSearch(); |
michael@0 | 1373 | return; |
michael@0 | 1374 | } |
michael@0 | 1375 | |
michael@0 | 1376 | // Hosts have no "/" in them. |
michael@0 | 1377 | let lastSlashIndex = this._currentSearchString.lastIndexOf("/"); |
michael@0 | 1378 | |
michael@0 | 1379 | // Search only URLs if there's a slash in the search string... |
michael@0 | 1380 | if (lastSlashIndex != -1) { |
michael@0 | 1381 | // ...but not if it's exactly at the end of the search string. |
michael@0 | 1382 | if (lastSlashIndex < this._currentSearchString.length - 1) |
michael@0 | 1383 | this._queryURL(); |
michael@0 | 1384 | else |
michael@0 | 1385 | this._finishSearch(); |
michael@0 | 1386 | return; |
michael@0 | 1387 | } |
michael@0 | 1388 | |
michael@0 | 1389 | // Do a synchronous search on the table of hosts. |
michael@0 | 1390 | let query = this._hostQuery; |
michael@0 | 1391 | query.params.search_string = this._currentSearchString.toLowerCase(); |
michael@0 | 1392 | // This is just to measure the delay to reach the UI, not the query time. |
michael@0 | 1393 | TelemetryStopwatch.start(DOMAIN_QUERY_TELEMETRY); |
michael@0 | 1394 | let ac = this; |
michael@0 | 1395 | let wrapper = new AutoCompleteStatementCallbackWrapper(this, { |
michael@0 | 1396 | handleResult: function (aResultSet) { |
michael@0 | 1397 | let row = aResultSet.getNextRow(); |
michael@0 | 1398 | let trimmedHost = row.getResultByIndex(0); |
michael@0 | 1399 | let untrimmedHost = row.getResultByIndex(1); |
michael@0 | 1400 | // If the untrimmed value doesn't preserve the user's input just |
michael@0 | 1401 | // ignore it and complete to the found host. |
michael@0 | 1402 | if (untrimmedHost && |
michael@0 | 1403 | !untrimmedHost.toLowerCase().contains(ac._originalSearchString.toLowerCase())) { |
michael@0 | 1404 | untrimmedHost = null; |
michael@0 | 1405 | } |
michael@0 | 1406 | |
michael@0 | 1407 | ac._result.appendMatch(ac._strippedPrefix + trimmedHost, "", "", "", untrimmedHost); |
michael@0 | 1408 | |
michael@0 | 1409 | // handleCompletion() will cause the result listener to be called, and |
michael@0 | 1410 | // will display the result in the UI. |
michael@0 | 1411 | }, |
michael@0 | 1412 | |
michael@0 | 1413 | handleError: function (aError) { |
michael@0 | 1414 | Components.utils.reportError( |
michael@0 | 1415 | "URL Inline Complete: An async statement encountered an " + |
michael@0 | 1416 | "error: " + aError.result + ", '" + aError.message + "'"); |
michael@0 | 1417 | }, |
michael@0 | 1418 | |
michael@0 | 1419 | handleCompletion: function (aReason) { |
michael@0 | 1420 | TelemetryStopwatch.finish(DOMAIN_QUERY_TELEMETRY); |
michael@0 | 1421 | ac._finishSearch(); |
michael@0 | 1422 | } |
michael@0 | 1423 | }, this._db); |
michael@0 | 1424 | this._pendingQuery = wrapper.executeAsync([query]); |
michael@0 | 1425 | }, |
michael@0 | 1426 | |
michael@0 | 1427 | /** |
michael@0 | 1428 | * Execute an asynchronous search through places, and complete |
michael@0 | 1429 | * up to the next URL separator. |
michael@0 | 1430 | */ |
michael@0 | 1431 | _queryURL: function UIC__queryURL() |
michael@0 | 1432 | { |
michael@0 | 1433 | // The URIs in the database are fixed up, so we can match on a lowercased |
michael@0 | 1434 | // host, but the path must be matched in a case sensitive way. |
michael@0 | 1435 | let pathIndex = |
michael@0 | 1436 | this._originalSearchString.indexOf("/", this._strippedPrefix.length); |
michael@0 | 1437 | this._currentSearchString = fixupSearchText( |
michael@0 | 1438 | this._originalSearchString.slice(0, pathIndex).toLowerCase() + |
michael@0 | 1439 | this._originalSearchString.slice(pathIndex) |
michael@0 | 1440 | ); |
michael@0 | 1441 | |
michael@0 | 1442 | // Within the standard autocomplete query, we only search the beginning |
michael@0 | 1443 | // of URLs for 1 result. |
michael@0 | 1444 | let query = this._urlQuery; |
michael@0 | 1445 | let (params = query.params) { |
michael@0 | 1446 | params.matchBehavior = MATCH_BEGINNING_CASE_SENSITIVE; |
michael@0 | 1447 | params.searchBehavior = Ci.mozIPlacesAutoComplete["BEHAVIOR_URL"]; |
michael@0 | 1448 | params.searchString = this._currentSearchString; |
michael@0 | 1449 | } |
michael@0 | 1450 | |
michael@0 | 1451 | // Execute the query. |
michael@0 | 1452 | let ac = this; |
michael@0 | 1453 | let wrapper = new AutoCompleteStatementCallbackWrapper(this, { |
michael@0 | 1454 | handleResult: function(aResultSet) { |
michael@0 | 1455 | let row = aResultSet.getNextRow(); |
michael@0 | 1456 | let value = row.getResultByIndex(0); |
michael@0 | 1457 | let url = fixupSearchText(value); |
michael@0 | 1458 | |
michael@0 | 1459 | let prefix = value.slice(0, value.length - stripPrefix(value).length); |
michael@0 | 1460 | |
michael@0 | 1461 | // We must complete the URL up to the next separator (which is /, ? or #). |
michael@0 | 1462 | let separatorIndex = url.slice(ac._currentSearchString.length) |
michael@0 | 1463 | .search(/[\/\?\#]/); |
michael@0 | 1464 | if (separatorIndex != -1) { |
michael@0 | 1465 | separatorIndex += ac._currentSearchString.length; |
michael@0 | 1466 | if (url[separatorIndex] == "/") { |
michael@0 | 1467 | separatorIndex++; // Include the "/" separator |
michael@0 | 1468 | } |
michael@0 | 1469 | url = url.slice(0, separatorIndex); |
michael@0 | 1470 | } |
michael@0 | 1471 | |
michael@0 | 1472 | // Add the result. |
michael@0 | 1473 | // If the untrimmed value doesn't preserve the user's input just |
michael@0 | 1474 | // ignore it and complete to the found url. |
michael@0 | 1475 | let untrimmedURL = prefix + url; |
michael@0 | 1476 | if (untrimmedURL && |
michael@0 | 1477 | !untrimmedURL.toLowerCase().contains(ac._originalSearchString.toLowerCase())) { |
michael@0 | 1478 | untrimmedURL = null; |
michael@0 | 1479 | } |
michael@0 | 1480 | |
michael@0 | 1481 | ac._result.appendMatch(ac._strippedPrefix + url, "", "", "", untrimmedURL); |
michael@0 | 1482 | |
michael@0 | 1483 | // handleCompletion() will cause the result listener to be called, and |
michael@0 | 1484 | // will display the result in the UI. |
michael@0 | 1485 | }, |
michael@0 | 1486 | |
michael@0 | 1487 | handleError: function(aError) { |
michael@0 | 1488 | Components.utils.reportError( |
michael@0 | 1489 | "URL Inline Complete: An async statement encountered an " + |
michael@0 | 1490 | "error: " + aError.result + ", '" + aError.message + "'"); |
michael@0 | 1491 | }, |
michael@0 | 1492 | |
michael@0 | 1493 | handleCompletion: function(aReason) { |
michael@0 | 1494 | ac._finishSearch(); |
michael@0 | 1495 | } |
michael@0 | 1496 | }, this._db); |
michael@0 | 1497 | this._pendingQuery = wrapper.executeAsync([query]); |
michael@0 | 1498 | }, |
michael@0 | 1499 | |
michael@0 | 1500 | stopSearch: function UIC_stopSearch() |
michael@0 | 1501 | { |
michael@0 | 1502 | delete this._originalSearchString; |
michael@0 | 1503 | delete this._currentSearchString; |
michael@0 | 1504 | delete this._result; |
michael@0 | 1505 | delete this._listener; |
michael@0 | 1506 | |
michael@0 | 1507 | if (this._pendingQuery) { |
michael@0 | 1508 | this._pendingQuery.cancel(); |
michael@0 | 1509 | delete this._pendingQuery; |
michael@0 | 1510 | } |
michael@0 | 1511 | }, |
michael@0 | 1512 | |
michael@0 | 1513 | /** |
michael@0 | 1514 | * Loads the preferences that we care about. |
michael@0 | 1515 | * |
michael@0 | 1516 | * @param [optional] aRegisterObserver |
michael@0 | 1517 | * Indicates if the preference observer should be added or not. The |
michael@0 | 1518 | * default value is false. |
michael@0 | 1519 | */ |
michael@0 | 1520 | _loadPrefs: function UIC_loadPrefs(aRegisterObserver) |
michael@0 | 1521 | { |
michael@0 | 1522 | let prefBranch = Services.prefs.getBranch(kBrowserUrlbarBranch); |
michael@0 | 1523 | let autocomplete = safePrefGetter(prefBranch, |
michael@0 | 1524 | kBrowserUrlbarAutocompleteEnabledPref, |
michael@0 | 1525 | true); |
michael@0 | 1526 | let autofill = safePrefGetter(prefBranch, |
michael@0 | 1527 | kBrowserUrlbarAutofillPref, |
michael@0 | 1528 | true); |
michael@0 | 1529 | this._autofillEnabled = autocomplete && autofill; |
michael@0 | 1530 | this._autofillTyped = safePrefGetter(prefBranch, |
michael@0 | 1531 | kBrowserUrlbarAutofillTypedPref, |
michael@0 | 1532 | true); |
michael@0 | 1533 | if (aRegisterObserver) { |
michael@0 | 1534 | Services.prefs.addObserver(kBrowserUrlbarBranch, this, true); |
michael@0 | 1535 | } |
michael@0 | 1536 | }, |
michael@0 | 1537 | |
michael@0 | 1538 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1539 | //// nsIAutoCompleteSearchDescriptor |
michael@0 | 1540 | get searchType() Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE, |
michael@0 | 1541 | |
michael@0 | 1542 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1543 | //// nsIObserver |
michael@0 | 1544 | |
michael@0 | 1545 | observe: function UIC_observe(aSubject, aTopic, aData) |
michael@0 | 1546 | { |
michael@0 | 1547 | if (aTopic == kTopicShutdown) { |
michael@0 | 1548 | this._closeDatabase(); |
michael@0 | 1549 | } |
michael@0 | 1550 | else if (aTopic == kPrefChanged && |
michael@0 | 1551 | (aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillPref || |
michael@0 | 1552 | aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutocompleteEnabledPref || |
michael@0 | 1553 | aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillTypedPref)) { |
michael@0 | 1554 | let previousAutofillTyped = this._autofillTyped; |
michael@0 | 1555 | this._loadPrefs(); |
michael@0 | 1556 | if (!this._autofillEnabled) { |
michael@0 | 1557 | this.stopSearch(); |
michael@0 | 1558 | this._closeDatabase(); |
michael@0 | 1559 | } |
michael@0 | 1560 | else if (this._autofillTyped != previousAutofillTyped) { |
michael@0 | 1561 | // Invalidate the statements to update them for the new typed status. |
michael@0 | 1562 | this._invalidateStatements(); |
michael@0 | 1563 | } |
michael@0 | 1564 | } |
michael@0 | 1565 | }, |
michael@0 | 1566 | |
michael@0 | 1567 | /** |
michael@0 | 1568 | * Finalizes and invalidates cached statements. |
michael@0 | 1569 | */ |
michael@0 | 1570 | _invalidateStatements: function UIC_invalidateStatements() |
michael@0 | 1571 | { |
michael@0 | 1572 | // Finalize the statements that we have used. |
michael@0 | 1573 | let stmts = [ |
michael@0 | 1574 | "__hostQuery", |
michael@0 | 1575 | "__urlQuery", |
michael@0 | 1576 | ]; |
michael@0 | 1577 | for (let i = 0; i < stmts.length; i++) { |
michael@0 | 1578 | // We do not want to create any query we haven't already created, so |
michael@0 | 1579 | // see if it is a getter first. |
michael@0 | 1580 | if (this[stmts[i]]) { |
michael@0 | 1581 | this[stmts[i]].finalize(); |
michael@0 | 1582 | this[stmts[i]] = null; |
michael@0 | 1583 | } |
michael@0 | 1584 | } |
michael@0 | 1585 | }, |
michael@0 | 1586 | |
michael@0 | 1587 | /** |
michael@0 | 1588 | * Closes the database. |
michael@0 | 1589 | */ |
michael@0 | 1590 | _closeDatabase: function UIC_closeDatabase() |
michael@0 | 1591 | { |
michael@0 | 1592 | this._invalidateStatements(); |
michael@0 | 1593 | if (this.__db) { |
michael@0 | 1594 | this._db.asyncClose(); |
michael@0 | 1595 | this.__db = null; |
michael@0 | 1596 | } |
michael@0 | 1597 | }, |
michael@0 | 1598 | |
michael@0 | 1599 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1600 | //// urlInlineComplete |
michael@0 | 1601 | |
michael@0 | 1602 | _finishSearch: function UIC_finishSearch() |
michael@0 | 1603 | { |
michael@0 | 1604 | // Notify the result object |
michael@0 | 1605 | let result = this._result; |
michael@0 | 1606 | |
michael@0 | 1607 | if (result.matchCount) { |
michael@0 | 1608 | result.setDefaultIndex(0); |
michael@0 | 1609 | result.setSearchResult(Ci.nsIAutoCompleteResult["RESULT_SUCCESS"]); |
michael@0 | 1610 | } else { |
michael@0 | 1611 | result.setDefaultIndex(-1); |
michael@0 | 1612 | result.setSearchResult(Ci.nsIAutoCompleteResult["RESULT_NOMATCH"]); |
michael@0 | 1613 | } |
michael@0 | 1614 | |
michael@0 | 1615 | this._listener.onSearchResult(this, result); |
michael@0 | 1616 | this.stopSearch(); |
michael@0 | 1617 | }, |
michael@0 | 1618 | |
michael@0 | 1619 | isSearchComplete: function UIC_isSearchComplete() |
michael@0 | 1620 | { |
michael@0 | 1621 | return this._pendingQuery == null; |
michael@0 | 1622 | }, |
michael@0 | 1623 | |
michael@0 | 1624 | isPendingSearch: function UIC_isPendingSearch(aHandle) |
michael@0 | 1625 | { |
michael@0 | 1626 | return this._pendingQuery == aHandle; |
michael@0 | 1627 | }, |
michael@0 | 1628 | |
michael@0 | 1629 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1630 | //// nsISupports |
michael@0 | 1631 | |
michael@0 | 1632 | classID: Components.ID("c88fae2d-25cf-4338-a1f4-64a320ea7440"), |
michael@0 | 1633 | |
michael@0 | 1634 | _xpcom_factory: XPCOMUtils.generateSingletonFactory(urlInlineComplete), |
michael@0 | 1635 | |
michael@0 | 1636 | QueryInterface: XPCOMUtils.generateQI([ |
michael@0 | 1637 | Ci.nsIAutoCompleteSearch, |
michael@0 | 1638 | Ci.nsIAutoCompleteSearchDescriptor, |
michael@0 | 1639 | Ci.nsIObserver, |
michael@0 | 1640 | Ci.nsISupportsWeakReference, |
michael@0 | 1641 | ]) |
michael@0 | 1642 | }; |
michael@0 | 1643 | |
michael@0 | 1644 | let components = [nsPlacesAutoComplete, urlInlineComplete]; |
michael@0 | 1645 | this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); |