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