toolkit/components/places/nsPlacesAutoComplete.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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);
  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;
  1016     return query;
  1017   },
  1019   /**
  1020    * Obtains the adaptive query with the properly bound parameters.
  1022    * @return the bound adaptive query.
  1023    */
  1024   _getBoundAdaptiveQuery: function PAC_getBoundAdaptiveQuery(aMatchBehavior)
  1026     // If we were not given a match behavior, use the stored match behavior.
  1027     if (arguments.length == 0) {
  1028       aMatchBehavior = this._matchBehavior;
  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;
  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.
  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)
  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;
  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";
  1088       else {
  1089         title = entryTitle;
  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";
  1104     // If we have tags and should show them, we need to add them to the title.
  1105     if (showTags) {
  1106       title += kTitleTagsSeparator + entryTags;
  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";
  1118       else if (entryBookmarked) {
  1119         style = "bookmark";
  1121       else {
  1122         style = "favicon";
  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.
  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.
  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)
  1148     if (aPlaceId && aPlaceId in this._usedPlaces) {
  1149       return true;
  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.
  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)
  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;
  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.
  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)
  1201     return (this._behavior &
  1202             Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()]);
  1203   },
  1205   /**
  1206    * Enables the desired AutoComplete behavior.
  1208    * @param aType
  1209    *        The behavior type to set.
  1210    */
  1211   _setBehavior: function PAC_setBehavior(aType)
  1213     this._behavior |=
  1214       Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()];
  1215   },
  1217   /**
  1218    * Determines if we are done searching or not.
  1220    * @return true if we have completed searching, false otherwise.
  1221    */
  1222   isSearchComplete: function PAC_isSearchComplete()
  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.
  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)
  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()
  1266   this._loadPrefs(true);
  1267   Services.obs.addObserver(this, kTopicShutdown, true);
  1270 urlInlineComplete.prototype = {
  1272 /////////////////////////////////////////////////////////////////////////////////
  1273 //// Database and query getters
  1275   __db: null,
  1277   get _db()
  1279     if (!this.__db && this._autofillEnabled) {
  1280       this.__db = PlacesUtils.history.DBConnection.clone(true);
  1282     return this.__db;
  1283   },
  1285   __hostQuery: null,
  1287   get _hostQuery()
  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       );
  1303     return this.__hostQuery;
  1304   },
  1306   __urlQuery: null,
  1308   get _urlQuery()
  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       );
  1325     return this.__urlQuery;
  1326   },
  1328   //////////////////////////////////////////////////////////////////////////////
  1329   //// nsIAutoCompleteSearch
  1331   startSearch: function UIC_startSearch(aSearchString, aSearchParam,
  1332                                         aPreviousResult, aListener)
  1334     // Stop the search in case the controller has not taken care of it.
  1335     if (this._pendingQuery) {
  1336       this.stopSearch();
  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;
  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;
  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;
  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;
  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();
  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()
  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;
  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
  1469           url = url.slice(0, separatorIndex);
  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;
  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();
  1496     }, this._db);
  1497     this._pendingQuery = wrapper.executeAsync([query]);
  1498   },
  1500   stopSearch: function UIC_stopSearch()
  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;
  1511   },
  1513   /**
  1514    * Loads the preferences that we care about.
  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)
  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);
  1536   },
  1538   //////////////////////////////////////////////////////////////////////////////
  1539   //// nsIAutoCompleteSearchDescriptor
  1540   get searchType() Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE,
  1542   //////////////////////////////////////////////////////////////////////////////
  1543   //// nsIObserver
  1545   observe: function UIC_observe(aSubject, aTopic, aData)
  1547     if (aTopic == kTopicShutdown) {
  1548       this._closeDatabase();
  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();
  1560       else if (this._autofillTyped != previousAutofillTyped) {
  1561         // Invalidate the statements to update them for the new typed status.
  1562         this._invalidateStatements();
  1565   },
  1567   /**
  1568    * Finalizes and invalidates cached statements.
  1569    */
  1570   _invalidateStatements: function UIC_invalidateStatements()
  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;
  1585   },
  1587   /**
  1588    * Closes the database.
  1589    */
  1590   _closeDatabase: function UIC_closeDatabase()
  1592     this._invalidateStatements();
  1593     if (this.__db) {
  1594       this._db.asyncClose();
  1595       this.__db = null;
  1597   },
  1599   //////////////////////////////////////////////////////////////////////////////
  1600   //// urlInlineComplete
  1602   _finishSearch: function UIC_finishSearch()
  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"]);
  1615     this._listener.onSearchResult(this, result);
  1616     this.stopSearch();
  1617   },
  1619   isSearchComplete: function UIC_isSearchComplete()
  1621     return this._pendingQuery == null;
  1622   },
  1624   isPendingSearch: function UIC_isPendingSearch(aHandle)
  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);

mercurial