toolkit/components/places/UnifiedComplete.js

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

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

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

     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 "use strict";
     9 ////////////////////////////////////////////////////////////////////////////////
    10 //// Constants
    12 const Cc = Components.classes;
    13 const Ci = Components.interfaces;
    14 const Cr = Components.results;
    15 const Cu = Components.utils;
    17 const TOPIC_SHUTDOWN = "places-shutdown";
    18 const TOPIC_PREFCHANGED = "nsPref:changed";
    20 const DEFAULT_BEHAVIOR = 0;
    22 const PREF_BRANCH = "browser.urlbar";
    24 // Prefs are defined as [pref name, default value].
    25 const PREF_ENABLED =            [ "autocomplete.enabled", true ];
    26 const PREF_AUTOFILL =           [ "autoFill",             true ];
    27 const PREF_AUTOFILL_TYPED =     [ "autoFill.typed",       true ];
    28 const PREF_AUTOFILL_PRIORITY =  [ "autoFill.priority",    true ];
    29 const PREF_DELAY =              [ "delay",                  50 ];
    30 const PREF_BEHAVIOR =           [ "matchBehavior", MATCH_BOUNDARY_ANYWHERE ];
    31 const PREF_DEFAULT_BEHAVIOR =   [ "default.behavior", DEFAULT_BEHAVIOR ];
    32 const PREF_EMPTY_BEHAVIOR =     [ "default.behavior.emptyRestriction",
    33                                   Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY |
    34                                   Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED ];
    35 const PREF_FILTER_JS =          [ "filter.javascript",    true ];
    36 const PREF_MAXRESULTS =         [ "maxRichResults",         25 ];
    37 const PREF_RESTRICT_HISTORY =   [ "restrict.history",      "^" ];
    38 const PREF_RESTRICT_BOOKMARKS = [ "restrict.bookmark",     "*" ];
    39 const PREF_RESTRICT_TYPED =     [ "restrict.typed",        "~" ];
    40 const PREF_RESTRICT_TAG =       [ "restrict.tag",          "+" ];
    41 const PREF_RESTRICT_SWITCHTAB = [ "restrict.openpage",     "%" ];
    42 const PREF_MATCH_TITLE =        [ "match.title",           "#" ];
    43 const PREF_MATCH_URL =          [ "match.url",             "@" ];
    45 // Match type constants.
    46 // These indicate what type of search function we should be using.
    47 const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
    48 const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE;
    49 const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
    50 const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING;
    51 const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE;
    53 // AutoComplete query type constants.
    54 // Describes the various types of queries that we can process rows for.
    55 const QUERYTYPE_KEYWORD       = 0;
    56 const QUERYTYPE_FILTERED      = 1;
    57 const QUERYTYPE_AUTOFILL_HOST = 2;
    58 const QUERYTYPE_AUTOFILL_URL  = 3;
    60 // This separator is used as an RTL-friendly way to split the title and tags.
    61 // It can also be used by an nsIAutoCompleteResult consumer to re-split the
    62 // "comment" back into the title and the tag.
    63 const TITLE_TAGS_SEPARATOR = " \u2013 ";
    65 // Telemetry probes.
    66 const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
    68 // The default frecency value used when inserting priority results.
    69 const FRECENCY_PRIORITY_DEFAULT = 1000;
    71 // Sqlite result row index constants.
    72 const QUERYINDEX_QUERYTYPE     = 0;
    73 const QUERYINDEX_URL           = 1;
    74 const QUERYINDEX_TITLE         = 2;
    75 const QUERYINDEX_ICONURL       = 3;
    76 const QUERYINDEX_BOOKMARKED    = 4;
    77 const QUERYINDEX_BOOKMARKTITLE = 5;
    78 const QUERYINDEX_TAGS          = 6;
    79 const QUERYINDEX_VISITCOUNT    = 7;
    80 const QUERYINDEX_TYPED         = 8;
    81 const QUERYINDEX_PLACEID       = 9;
    82 const QUERYINDEX_SWITCHTAB     = 10;
    83 const QUERYINDEX_FRECENCY      = 11;
    85 // This SQL query fragment provides the following:
    86 //   - whether the entry is bookmarked (QUERYINDEX_BOOKMARKED)
    87 //   - the bookmark title, if it is a bookmark (QUERYINDEX_BOOKMARKTITLE)
    88 //   - the tags associated with a bookmarked entry (QUERYINDEX_TAGS)
    89 const SQL_BOOKMARK_TAGS_FRAGMENT = sql(
    90   "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked,",
    91   "( SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL",
    92     "ORDER BY lastModified DESC LIMIT 1",
    93   ") AS btitle,",
    94   "( SELECT GROUP_CONCAT(t.title, ',')",
    95     "FROM moz_bookmarks b",
    96     "JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent",
    97     "WHERE b.fk = h.id",
    98   ") AS tags");
   100 // TODO bug 412736: in case of a frecency tie, we might break it with h.typed
   101 // and h.visit_count.  That is slower though, so not doing it yet...
   102 const SQL_DEFAULT_QUERY = sql(
   103   "SELECT :query_type, h.url, h.title, f.url,", SQL_BOOKMARK_TAGS_FRAGMENT, ",",
   104          "h.visit_count, h.typed, h.id, t.open_count, h.frecency",
   105   "FROM moz_places h",
   106   "LEFT JOIN moz_favicons f ON f.id = h.favicon_id",
   107   "LEFT JOIN moz_openpages_temp t ON t.url = h.url",
   108   "WHERE h.frecency <> 0",
   109     "AND AUTOCOMPLETE_MATCH(:searchString, h.url,",
   110                            "IFNULL(btitle, h.title), tags,",
   111                            "h.visit_count, h.typed,",
   112                            "bookmarked, t.open_count,",
   113                            ":matchBehavior, :searchBehavior)",
   114     "/*CONDITIONS*/",
   115   "ORDER BY h.frecency DESC, h.id DESC",
   116   "LIMIT :maxResults");
   118 // Enforce ignoring the visit_count index, since the frecency one is much
   119 // faster in this case.  ANALYZE helps the query planner to figure out the
   120 // faster path, but it may not have up-to-date information yet.
   121 const SQL_HISTORY_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
   122                                                     "AND +h.visit_count > 0", "g");
   124 const SQL_BOOKMARK_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
   125                                                      "AND bookmarked", "g");
   127 const SQL_TAGS_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
   128                                                  "AND tags NOTNULL", "g");
   130 const SQL_TYPED_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
   131                                                   "AND h.typed = 1", "g");
   133 const SQL_SWITCHTAB_QUERY = sql(
   134   "SELECT :query_type, t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL,",
   135          "t.open_count, NULL",
   136   "FROM moz_openpages_temp t",
   137   "LEFT JOIN moz_places h ON h.url = t.url",
   138   "WHERE h.id IS NULL",
   139     "AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL,",
   140                             "NULL, NULL, NULL, t.open_count,",
   141                             ":matchBehavior, :searchBehavior)",
   142   "ORDER BY t.ROWID DESC",
   143   "LIMIT :maxResults");
   145 const SQL_ADAPTIVE_QUERY = sql(
   146   "/* do not warn (bug 487789) */",
   147   "SELECT :query_type, h.url, h.title, f.url,", SQL_BOOKMARK_TAGS_FRAGMENT, ",",
   148          "h.visit_count, h.typed, h.id, t.open_count, h.frecency",
   149   "FROM (",
   150     "SELECT ROUND(MAX(use_count) * (1 + (input = :search_string)), 1) AS rank,",
   151            "place_id",
   152     "FROM moz_inputhistory",
   153     "WHERE input BETWEEN :search_string AND :search_string || X'FFFF'",
   154     "GROUP BY place_id",
   155   ") AS i",
   156   "JOIN moz_places h ON h.id = i.place_id",
   157   "LEFT JOIN moz_favicons f ON f.id = h.favicon_id",
   158   "LEFT JOIN moz_openpages_temp t ON t.url = h.url",
   159   "WHERE AUTOCOMPLETE_MATCH(NULL, h.url,",
   160                            "IFNULL(btitle, h.title), tags,",
   161                            "h.visit_count, h.typed, bookmarked,",
   162                            "t.open_count,",
   163                            ":matchBehavior, :searchBehavior)",
   164   "ORDER BY rank DESC, h.frecency DESC");
   166 const SQL_KEYWORD_QUERY = sql(
   167   "/* do not warn (bug 487787) */",
   168   "SELECT :query_type,",
   169     "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk)",
   170     "AS search_url, h.title,",
   171     "IFNULL(f.url, (SELECT f.url",
   172                    "FROM moz_places",
   173                    "JOIN moz_favicons f ON f.id = favicon_id",
   174                    "WHERE rev_host = (SELECT rev_host FROM moz_places WHERE id = b.fk)",
   175                    "ORDER BY frecency DESC",
   176                    "LIMIT 1)",
   177           "),",
   178     "1, b.title, NULL, h.visit_count, h.typed, IFNULL(h.id, b.fk),",
   179     "t.open_count, h.frecency",
   180   "FROM moz_keywords k",
   181   "JOIN moz_bookmarks b ON b.keyword_id = k.id",
   182   "LEFT JOIN moz_places h ON h.url = search_url",
   183   "LEFT JOIN moz_favicons f ON f.id = h.favicon_id",
   184   "LEFT JOIN moz_openpages_temp t ON t.url = search_url",
   185   "WHERE LOWER(k.keyword) = LOWER(:keyword)",
   186   "ORDER BY h.frecency DESC");
   188 const SQL_HOST_QUERY = sql(
   189   "/* do not warn (bug NA): not worth to index on (typed, frecency) */",
   190   "SELECT :query_type, host || '/', prefix || host || '/',",
   191          "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, frecency",
   192   "FROM moz_hosts",
   193   "WHERE host BETWEEN :searchString AND :searchString || X'FFFF'",
   194   "AND frecency <> 0",
   195   "/*CONDITIONS*/",
   196   "ORDER BY frecency DESC",
   197   "LIMIT 1");
   199 const SQL_TYPED_HOST_QUERY = SQL_HOST_QUERY.replace("/*CONDITIONS*/",
   200                                                     "AND typed = 1");
   201 const SQL_URL_QUERY = sql(
   202   "/* do not warn (bug no): cannot use an index */",
   203   "SELECT :query_type, h.url,",
   204          "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, h.frecency",
   205   "FROM moz_places h",
   206   "WHERE h.frecency <> 0",
   207   "/*CONDITIONS*/",
   208   "AND AUTOCOMPLETE_MATCH(:searchString, h.url,",
   209   "h.title, '',",
   210   "h.visit_count, h.typed, 0, 0,",
   211   ":matchBehavior, :searchBehavior)",
   212   "ORDER BY h.frecency DESC, h.id DESC",
   213   "LIMIT 1");
   215 const SQL_TYPED_URL_QUERY = SQL_URL_QUERY.replace("/*CONDITIONS*/",
   216                                                   "AND typed = 1");
   218 ////////////////////////////////////////////////////////////////////////////////
   219 //// Getters
   221 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
   222 Cu.import("resource://gre/modules/Services.jsm");
   224 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
   225                                   "resource://gre/modules/PlacesUtils.jsm");
   226 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
   227                                   "resource://gre/modules/TelemetryStopwatch.jsm");
   228 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
   229                                   "resource://gre/modules/NetUtil.jsm");
   230 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
   231                                   "resource://gre/modules/Preferences.jsm");
   232 XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
   233                                   "resource://gre/modules/Sqlite.jsm");
   234 XPCOMUtils.defineLazyModuleGetter(this, "OS",
   235                                   "resource://gre/modules/osfile.jsm");
   236 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
   237                                   "resource://gre/modules/Promise.jsm");
   238 XPCOMUtils.defineLazyModuleGetter(this, "Task",
   239                                   "resource://gre/modules/Task.jsm");
   240 XPCOMUtils.defineLazyModuleGetter(this, "PriorityUrlProvider",
   241                                   "resource://gre/modules/PriorityUrlProvider.jsm");
   243 XPCOMUtils.defineLazyServiceGetter(this, "textURIService",
   244                                    "@mozilla.org/intl/texttosuburi;1",
   245                                    "nsITextToSubURI");
   247 /**
   248  * Storage object for switch-to-tab entries.
   249  * This takes care of caching and registering open pages, that will be reused
   250  * by switch-to-tab queries.  It has an internal cache, so that the Sqlite
   251  * store is lazy initialized only on first use.
   252  * It has a simple API:
   253  *   initDatabase(conn): initializes the temporary Sqlite entities to store data
   254  *   add(uri): adds a given nsIURI to the store
   255  *   delete(uri): removes a given nsIURI from the store
   256  *   shutdown(): stops storing data to Sqlite
   257  */
   258 XPCOMUtils.defineLazyGetter(this, "SwitchToTabStorage", () => Object.seal({
   259   _conn: null,
   260   // Temporary queue used while the database connection is not available.
   261   _queue: new Set(),
   262   initDatabase: Task.async(function* (conn) {
   263     // To reduce IO use an in-memory table for switch-to-tab tracking.
   264     // Note: this should be kept up-to-date with the definition in
   265     //       nsPlacesTables.h.
   266     yield conn.execute(sql(
   267       "CREATE TEMP TABLE moz_openpages_temp (",
   268         "url TEXT PRIMARY KEY,",
   269         "open_count INTEGER",
   270       ")"));
   272     // Note: this should be kept up-to-date with the definition in
   273     //       nsPlacesTriggers.h.
   274     yield conn.execute(sql(
   275       "CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger",
   276       "AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW",
   277       "WHEN NEW.open_count = 0",
   278       "BEGIN",
   279         "DELETE FROM moz_openpages_temp",
   280         "WHERE url = NEW.url;",
   281       "END"));
   283     this._conn = conn;
   285     // Populate the table with the current cache contents...
   286     this._queue.forEach(this.add, this);
   287     // ...then clear it to avoid double additions.
   288     this._queue.clear();
   289   }),
   291   add: function (uri) {
   292     if (!this._conn) {
   293       this._queue.add(uri);
   294       return;
   295     }
   296     this._conn.executeCached(sql(
   297       "INSERT OR REPLACE INTO moz_openpages_temp (url, open_count)",
   298         "VALUES ( :url, IFNULL( (SELECT open_count + 1",
   299                                  "FROM moz_openpages_temp",
   300                                  "WHERE url = :url),",
   301                                  "1",
   302                              ")",
   303                ")"
   304     ), { url: uri.spec });
   305   },
   307   delete: function (uri) {
   308     if (!this._conn) {
   309       this._queue.delete(uri);
   310       return;
   311     }
   312     this._conn.executeCached(sql(
   313       "UPDATE moz_openpages_temp",
   314       "SET open_count = open_count - 1",
   315       "WHERE url = :url"
   316     ), { url: uri.spec });
   317   },
   319   shutdown: function () {
   320     this._conn = null;
   321     this._queue.clear();
   322   }
   323 }));
   325 /**
   326  * This helper keeps track of preferences and keeps their values up-to-date.
   327  */
   328 XPCOMUtils.defineLazyGetter(this, "Prefs", () => {
   329   let prefs = new Preferences(PREF_BRANCH);
   331   function loadPrefs() {
   332     store.enabled = prefs.get(...PREF_ENABLED);
   333     store.autofill = prefs.get(...PREF_AUTOFILL);
   334     store.autofillTyped = prefs.get(...PREF_AUTOFILL_TYPED);
   335     store.autofillPriority = prefs.get(...PREF_AUTOFILL_PRIORITY);
   336     store.delay = prefs.get(...PREF_DELAY);
   337     store.matchBehavior = prefs.get(...PREF_BEHAVIOR);
   338     store.filterJavaScript = prefs.get(...PREF_FILTER_JS);
   339     store.maxRichResults = prefs.get(...PREF_MAXRESULTS);
   340     store.restrictHistoryToken = prefs.get(...PREF_RESTRICT_HISTORY);
   341     store.restrictBookmarkToken = prefs.get(...PREF_RESTRICT_BOOKMARKS);
   342     store.restrictTypedToken = prefs.get(...PREF_RESTRICT_TYPED);
   343     store.restrictTagToken = prefs.get(...PREF_RESTRICT_TAG);
   344     store.restrictOpenPageToken = prefs.get(...PREF_RESTRICT_SWITCHTAB);
   345     store.matchTitleToken = prefs.get(...PREF_MATCH_TITLE);
   346     store.matchURLToken = prefs.get(...PREF_MATCH_URL);
   347     store.defaultBehavior = prefs.get(...PREF_DEFAULT_BEHAVIOR);
   348     // Further restrictions to apply for "empty searches" (i.e. searches for "").
   349     store.emptySearchDefaultBehavior = store.defaultBehavior |
   350                                        prefs.get(...PREF_EMPTY_BEHAVIOR);
   352     // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE.
   353     if (store.matchBehavior != MATCH_ANYWHERE &&
   354         store.matchBehavior != MATCH_BOUNDARY &&
   355         store.matchBehavior != MATCH_BEGINNING) {
   356       store.matchBehavior = MATCH_BOUNDARY_ANYWHERE;
   357     }
   359     store.tokenToBehaviorMap = new Map([
   360       [ store.restrictHistoryToken, "history" ],
   361       [ store.restrictBookmarkToken, "bookmark" ],
   362       [ store.restrictTagToken, "tag" ],
   363       [ store.restrictOpenPageToken, "openpage" ],
   364       [ store.matchTitleToken, "title" ],
   365       [ store.matchURLToken, "url" ],
   366       [ store.restrictTypedToken, "typed" ]
   367     ]);
   368   }
   370   let store = {
   371     observe: function (subject, topic, data) {
   372       loadPrefs();
   373     },
   374     QueryInterface: XPCOMUtils.generateQI([ Ci.nsIObserver ])
   375   };
   376   loadPrefs();
   377   prefs.observe("", store);
   379   return Object.seal(store);
   380 });
   382 ////////////////////////////////////////////////////////////////////////////////
   383 //// Helper functions
   385 /**
   386  * Joins multiple sql tokens into a single sql query.
   387  */
   388 function sql(...parts) parts.join(" ");
   390 /**
   391  * Used to unescape encoded URI strings and drop information that we do not
   392  * care about.
   393  *
   394  * @param spec
   395  *        The text to unescape and modify.
   396  * @return the modified spec.
   397  */
   398 function fixupSearchText(spec)
   399   textURIService.unEscapeURIForUI("UTF-8", stripPrefix(spec));
   401 /**
   402  * Generates the tokens used in searching from a given string.
   403  *
   404  * @param searchString
   405  *        The string to generate tokens from.
   406  * @return an array of tokens.
   407  * @note Calling split on an empty string will return an array containing one
   408  *       empty string.  We don't want that, as it'll break our logic, so return
   409  *       an empty array then.
   410  */
   411 function getUnfilteredSearchTokens(searchString)
   412   searchString.length ? searchString.split(" ") : [];
   414 /**
   415  * Strip prefixes from the URI that we don't care about for searching.
   416  *
   417  * @param spec
   418  *        The text to modify.
   419  * @return the modified spec.
   420  */
   421 function stripPrefix(spec)
   422 {
   423   ["http://", "https://", "ftp://"].some(scheme => {
   424     if (spec.startsWith(scheme)) {
   425       spec = spec.slice(scheme.length);
   426       return true;
   427     }
   428     return false;
   429   });
   431   if (spec.startsWith("www.")) {
   432     spec = spec.slice(4);
   433   }
   434   return spec;
   435 }
   437 ////////////////////////////////////////////////////////////////////////////////
   438 //// Search Class
   439 //// Manages a single instance of an autocomplete search.
   441 function Search(searchString, searchParam, autocompleteListener,
   442                 resultListener, autocompleteSearch) {
   443   // We want to store the original string with no leading or trailing
   444   // whitespace for case sensitive searches.
   445   this._originalSearchString = searchString.trim();
   446   this._searchString = fixupSearchText(this._originalSearchString.toLowerCase());
   447   this._searchTokens =
   448     this.filterTokens(getUnfilteredSearchTokens(this._searchString));
   449   // The protocol and the host are lowercased by nsIURI, so it's fine to
   450   // lowercase the typed prefix, to add it back to the results later.
   451   this._strippedPrefix = this._originalSearchString.slice(
   452     0, this._originalSearchString.length - this._searchString.length
   453   ).toLowerCase();
   454   // The URIs in the database are fixed-up, so we can match on a lowercased
   455   // host, but the path must be matched in a case sensitive way.
   456   let pathIndex =
   457     this._originalSearchString.indexOf("/", this._strippedPrefix.length);
   458   this._autofillUrlSearchString = fixupSearchText(
   459     this._originalSearchString.slice(0, pathIndex).toLowerCase() +
   460     this._originalSearchString.slice(pathIndex)
   461   );
   463   this._enableActions = searchParam.split(" ").indexOf("enable-actions") != -1;
   465   this._listener = autocompleteListener;
   466   this._autocompleteSearch = autocompleteSearch;
   468   this._matchBehavior = Prefs.matchBehavior;
   469   // Set the default behavior for this search.
   470   this._behavior = this._searchString ? Prefs.defaultBehavior
   471                                       : Prefs.emptySearchDefaultBehavior;
   472   // Create a new result to add eventual matches.  Note we need a result
   473   // regardless having matches.
   474   let result = Cc["@mozilla.org/autocomplete/simple-result;1"]
   475                  .createInstance(Ci.nsIAutoCompleteSimpleResult);
   476   result.setSearchString(searchString);
   477   result.setListener(resultListener);
   478   // Will be set later, if needed.
   479   result.setDefaultIndex(-1);
   480   this._result = result;
   482   // These are used to avoid adding duplicate entries to the results.
   483   this._usedURLs = new Set();
   484   this._usedPlaceIds = new Set();
   485 }
   487 Search.prototype = {
   488   /**
   489    * Enables the desired AutoComplete behavior.
   490    *
   491    * @param type
   492    *        The behavior type to set.
   493    */
   494   setBehavior: function (type) {
   495     this._behavior |=
   496       Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()];
   497   },
   499   /**
   500    * Determines if the specified AutoComplete behavior is set.
   501    *
   502    * @param aType
   503    *        The behavior type to test for.
   504    * @return true if the behavior is set, false otherwise.
   505    */
   506   hasBehavior: function (type) {
   507     return this._behavior &
   508            Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()];
   509   },
   511   /**
   512    * Used to delay the most complex queries, to save IO while the user is
   513    * typing.
   514    */
   515   _sleepDeferred: null,
   516   _sleep: function (aTimeMs) {
   517     // Reuse a single instance to try shaving off some usless work before
   518     // the first query.
   519     if (!this._sleepTimer)
   520       this._sleepTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   521     this._sleepDeferred = Promise.defer();
   522     this._sleepTimer.initWithCallback(() => this._sleepDeferred.resolve(),
   523                                       aTimeMs, Ci.nsITimer.TYPE_ONE_SHOT);
   524     return this._sleepDeferred.promise;
   525   },
   527   /**
   528    * Given an array of tokens, this function determines which query should be
   529    * ran.  It also removes any special search tokens.
   530    *
   531    * @param tokens
   532    *        An array of search tokens.
   533    * @return the filtered list of tokens to search with.
   534    */
   535   filterTokens: function (tokens) {
   536     // Set the proper behavior while filtering tokens.
   537     for (let i = tokens.length - 1; i >= 0; i--) {
   538       let behavior = Prefs.tokenToBehaviorMap.get(tokens[i]);
   539       // Don't remove the token if it didn't match, or if it's an action but
   540       // actions are not enabled.
   541       if (behavior && (behavior != "openpage" || this._enableActions)) {
   542         this.setBehavior(behavior);
   543         tokens.splice(i, 1);
   544       }
   545     }
   547     // Set the right JavaScript behavior based on our preference.  Note that the
   548     // preference is whether or not we should filter JavaScript, and the
   549     // behavior is if we should search it or not.
   550     if (!Prefs.filterJavaScript) {
   551       this.setBehavior("javascript");
   552     }
   554     return tokens;
   555   },
   557   /**
   558    * Used to cancel this search, will stop providing results.
   559    */
   560   cancel: function () {
   561     if (this._sleepTimer)
   562       this._sleepTimer.cancel();
   563     if (this._sleepDeferred) {
   564       this._sleepDeferred.resolve();
   565       this._sleepDeferred = null;
   566     }
   567     delete this._pendingQuery;
   568   },
   570   /**
   571    * Whether this search is running.
   572    */
   573   get pending() !!this._pendingQuery,
   575   /**
   576    * Execute the search and populate results.
   577    * @param conn
   578    *        The Sqlite connection.
   579    */
   580   execute: Task.async(function* (conn) {
   581     this._pendingQuery = true;
   582     TelemetryStopwatch.start(TELEMETRY_1ST_RESULT);
   584     // For any given search, we run many queries:
   585     // 1) priority domains
   586     // 2) inline completion
   587     // 3) keywords (this._keywordQuery)
   588     // 4) adaptive learning (this._adaptiveQuery)
   589     // 5) open pages not supported by history (this._switchToTabQuery)
   590     // 6) query based on match behavior
   591     //
   592     // (3) only gets ran if we get any filtered tokens, since if there are no
   593     // tokens, there is nothing to match.
   595     // Get the final query, based on the tokens found in the search string.
   596     let queries = [ this._adaptiveQuery,
   597                     this._switchToTabQuery,
   598                     this._searchQuery ];
   600     if (this._searchTokens.length == 1) {
   601       yield this._matchPriorityUrl();
   602     } else if (this._searchTokens.length > 1) {
   603       queries.unshift(this._keywordQuery);
   604     }
   606     if (this._shouldAutofill) {
   607       // Hosts have no "/" in them.
   608       let lastSlashIndex = this._searchString.lastIndexOf("/");
   609       // Search only URLs if there's a slash in the search string...
   610       if (lastSlashIndex != -1) {
   611         // ...but not if it's exactly at the end of the search string.
   612         if (lastSlashIndex < this._searchString.length - 1) {
   613           queries.unshift(this._urlQuery);
   614         }
   615       } else if (this.pending) {
   616         // The host query is executed immediately, while any other is delayed
   617         // to avoid overloading the connection.
   618         let [ query, params ] = this._hostQuery;
   619         yield conn.executeCached(query, params, this._onResultRow.bind(this));
   620       }
   621     }
   623     yield this._sleep(Prefs.delay);
   624     if (!this.pending)
   625       return;
   627     for (let [query, params] of queries) {
   628       yield conn.executeCached(query, params, this._onResultRow.bind(this));
   629       if (!this.pending)
   630         return;
   631     }
   633     // If we do not have enough results, and our match type is
   634     // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
   635     // results.
   636     if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE &&
   637         this._result.matchCount < Prefs.maxRichResults) {
   638       this._matchBehavior = MATCH_ANYWHERE;
   639       for (let [query, params] of [ this._adaptiveQuery,
   640                                     this._searchQuery ]) {
   641         yield conn.executeCached(query, params, this._onResultRow);
   642         if (!this.pending)
   643           return;
   644       }
   645     }
   647     // If we didn't find enough matches and we have some frecency-driven
   648     // matches, add them.
   649     if (this._frecencyMatches) {
   650       this._frecencyMatches.forEach(this._addMatch, this);
   651     }
   652   }),
   654   _matchPriorityUrl: function* () {
   655     if (!Prefs.autofillPriority)
   656       return;
   657     let priorityMatch = yield PriorityUrlProvider.getMatch(this._searchString);
   658     if (priorityMatch) {
   659       this._result.setDefaultIndex(0);
   660       this._addFrecencyMatch({
   661         value: priorityMatch.token,
   662         comment: priorityMatch.title,
   663         icon: priorityMatch.iconUrl,
   664         style: "priority-" + priorityMatch.reason,
   665         finalCompleteValue: priorityMatch.url,
   666         frecency: FRECENCY_PRIORITY_DEFAULT
   667       });
   668     }
   669   },
   671   _onResultRow: function (row) {
   672     TelemetryStopwatch.finish(TELEMETRY_1ST_RESULT);
   673     let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE);
   674     let match;
   675     switch (queryType) {
   676       case QUERYTYPE_AUTOFILL_HOST:
   677         this._result.setDefaultIndex(0);
   678         match = this._processHostRow(row);
   679         break;
   680       case QUERYTYPE_AUTOFILL_URL:
   681         this._result.setDefaultIndex(0);
   682         match = this._processUrlRow(row);
   683         break;
   684       case QUERYTYPE_FILTERED:
   685       case QUERYTYPE_KEYWORD:
   686         match = this._processRow(row);
   687         break;
   688     }
   689     this._addMatch(match);
   690   },
   692   /**
   693    * These matches should be mixed up with other matches, based on frecency.
   694    */
   695   _addFrecencyMatch: function (match) {
   696     if (!this._frecencyMatches)
   697       this._frecencyMatches = [];
   698     this._frecencyMatches.push(match);
   699     // We keep this array in reverse order, so we can walk it and remove stuff
   700     // from it in one pass.  Notice that for frecency reverse order means from
   701     // lower to higher.
   702     this._frecencyMatches.sort((a, b) => a.frecency - b.frecency);
   703   },
   705   _addMatch: function (match) {
   706     let notifyResults = false;
   708     if (this._frecencyMatches) {
   709       for (let i = this._frecencyMatches.length - 1;  i >= 0 ; i--) {
   710         if (this._frecencyMatches[i].frecency > match.frecency) {
   711           this._addMatch(this._frecencyMatches.splice(i, 1)[0]);
   712         }
   713       }
   714     }
   716     // Must check both id and url, cause keywords dinamically modify the url.
   717     if ((!match.placeId || !this._usedPlaceIds.has(match.placeId)) &&
   718         !this._usedURLs.has(stripPrefix(match.value))) {
   719       // Add this to our internal tracker to ensure duplicates do not end up in
   720       // the result.
   721       // Not all entries have a place id, thus we fallback to the url for them.
   722       // We cannot use only the url since keywords entries are modified to
   723       // include the search string, and would be returned multiple times.  Ids
   724       // are faster too.
   725       if (match.placeId)
   726         this._usedPlaceIds.add(match.placeId);
   727       this._usedURLs.add(stripPrefix(match.value));
   729       this._result.appendMatch(match.value,
   730                                match.comment,
   731                                match.icon || PlacesUtils.favicons.defaultFavicon.spec,
   732                                match.style || "favicon",
   733                                match.finalCompleteValue);
   734       notifyResults = true;
   735     }
   737     if (this._result.matchCount == Prefs.maxRichResults || !this.pending) {
   738       // We have enough results, so stop running our search.
   739       this.cancel();
   740       // This tells Sqlite.jsm to stop providing us results and cancel the
   741       // underlying query.
   742       throw StopIteration;
   743     }
   745     if (notifyResults) {
   746       // Notify about results if we've gotten them.
   747       this.notifyResults(true);
   748     }
   749   },
   751   _processHostRow: function (row) {
   752     let match = {};
   753     let trimmedHost = row.getResultByIndex(QUERYINDEX_URL);
   754     let untrimmedHost = row.getResultByIndex(QUERYINDEX_TITLE);
   755     let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
   756     // If the untrimmed value doesn't preserve the user's input just
   757     // ignore it and complete to the found host.
   758     if (untrimmedHost &&
   759         !untrimmedHost.toLowerCase().contains(this._originalSearchString.toLowerCase())) {
   760       // THIS CAUSES null TO BE SHOWN AS TITLE.
   761       untrimmedHost = null;
   762     }
   764     match.value = this._strippedPrefix + trimmedHost;
   765     match.comment = trimmedHost;
   766     match.finalCompleteValue = untrimmedHost;
   767     match.frecency = frecency;
   768     return match;
   769   },
   771   _processUrlRow: function (row) {
   772     let match = {};
   773     let value = row.getResultByIndex(QUERYINDEX_URL);
   774     let url = fixupSearchText(value);
   775     let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
   777     let prefix = value.slice(0, value.length - stripPrefix(value).length);
   779     // We must complete the URL up to the next separator (which is /, ? or #).
   780     let separatorIndex = url.slice(this._searchString.length)
   781                             .search(/[\/\?\#]/);
   782     if (separatorIndex != -1) {
   783       separatorIndex += this._searchString.length;
   784       if (url[separatorIndex] == "/") {
   785         separatorIndex++; // Include the "/" separator
   786       }
   787       url = url.slice(0, separatorIndex);
   788     }
   790     // If the untrimmed value doesn't preserve the user's input just
   791     // ignore it and complete to the found url.
   792     let untrimmedURL = prefix + url;
   793     if (untrimmedURL &&
   794         !untrimmedURL.toLowerCase().contains(this._originalSearchString.toLowerCase())) {
   795       // THIS CAUSES null TO BE SHOWN AS TITLE.
   796       untrimmedURL = null;
   797      }
   799     match.value = this._strippedPrefix + url;
   800     match.comment = url;
   801     match.finalCompleteValue = untrimmedURL;
   802     match.frecency = frecency;
   803     return match;
   804   },
   806   _processRow: function (row) {
   807     let match = {};
   808     match.placeId = row.getResultByIndex(QUERYINDEX_PLACEID);
   809     let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE);
   810     let escapedURL = row.getResultByIndex(QUERYINDEX_URL);
   811     let openPageCount = row.getResultByIndex(QUERYINDEX_SWITCHTAB) || 0;
   812     let historyTitle = row.getResultByIndex(QUERYINDEX_TITLE) || "";
   813     let iconurl = row.getResultByIndex(QUERYINDEX_ICONURL) || "";
   814     let bookmarked = row.getResultByIndex(QUERYINDEX_BOOKMARKED);
   815     let bookmarkTitle = bookmarked ?
   816       row.getResultByIndex(QUERYINDEX_BOOKMARKTITLE) : null;
   817     let tags = row.getResultByIndex(QUERYINDEX_TAGS) || "";
   818     let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
   820     // If actions are enabled and the page is open, add only the switch-to-tab
   821     // result.  Otherwise, add the normal result.
   822     let [url, action] = this._enableActions && openPageCount > 0 ?
   823                         ["moz-action:switchtab," + escapedURL, "action "] :
   824                         [escapedURL, ""];
   826     // Always prefer the bookmark title unless it is empty
   827     let title = bookmarkTitle || historyTitle;
   829     if (queryType == QUERYTYPE_KEYWORD) {
   830       // If we do not have a title, then we must have a keyword, so let the UI
   831       // know it is a keyword.  Otherwise, we found an exact page match, so just
   832       // show the page like a regular result.  Because the page title is likely
   833       // going to be more specific than the bookmark title (keyword title).
   834       if (!historyTitle) {
   835         match.style = "keyword";
   836       }
   837       else {
   838         title = historyTitle;
   839       }
   840     }
   842     // We will always prefer to show tags if we have them.
   843     let showTags = !!tags;
   845     // However, we'll act as if a page is not bookmarked or tagged if the user
   846     // only wants only history and not bookmarks or tags.
   847     if (this.hasBehavior("history") &&
   848         !(this.hasBehavior("bookmark") || this.hasBehavior("tag"))) {
   849       showTags = false;
   850       match.style = "favicon";
   851     }
   853     // If we have tags and should show them, we need to add them to the title.
   854     if (showTags) {
   855       title += TITLE_TAGS_SEPARATOR + tags;
   856     }
   858     // We have to determine the right style to display.  Tags show the tag icon,
   859     // bookmarks get the bookmark icon, and keywords get the keyword icon.  If
   860     // the result does not fall into any of those, it just gets the favicon.
   861     if (!match.style) {
   862       // It is possible that we already have a style set (from a keyword
   863       // search or because of the user's preferences), so only set it if we
   864       // haven't already done so.
   865       if (showTags) {
   866         match.style = "tag";
   867       }
   868       else if (bookmarked) {
   869         match.style = "bookmark";
   870       }
   871     }
   873     match.value = url;
   874     match.comment = title;
   875     if (iconurl) {
   876       match.icon = PlacesUtils.favicons
   877                               .getFaviconLinkForIcon(NetUtil.newURI(iconurl)).spec;
   878     }
   879     match.frecency = frecency;
   881     return match;
   882   },
   884   /**
   885    * Obtains the search query to be used based on the previously set search
   886    * behaviors (accessed by this.hasBehavior).
   887    *
   888    * @return an array consisting of the correctly optimized query to search the
   889    *         database with and an object containing the params to bound.
   890    */
   891   get _searchQuery() {
   892     // We use more optimized queries for restricted searches, so we will always
   893     // return the most restrictive one to the least restrictive one if more than
   894     // one token is found.
   895     // Note: "openpages" behavior is supported by the default query.
   896     //       _switchToTabQuery instead returns only pages not supported by
   897     //       history and it is always executed.
   898     let query = this.hasBehavior("tag") ? SQL_TAGS_QUERY :
   899                 this.hasBehavior("bookmark") ? SQL_BOOKMARK_QUERY :
   900                 this.hasBehavior("typed") ? SQL_TYPED_QUERY :
   901                 this.hasBehavior("history") ? SQL_HISTORY_QUERY :
   902                 SQL_DEFAULT_QUERY;
   904     return [
   905       query,
   906       {
   907         parent: PlacesUtils.tagsFolderId,
   908         query_type: QUERYTYPE_FILTERED,
   909         matchBehavior: this._matchBehavior,
   910         searchBehavior: this._behavior,
   911         // We only want to search the tokens that we are left with - not the
   912         // original search string.
   913         searchString: this._searchTokens.join(" "),
   914         // Limit the query to the the maximum number of desired results.
   915         // This way we can avoid doing more work than needed.
   916         maxResults: Prefs.maxRichResults
   917       }
   918     ];
   919   },
   921   /**
   922    * Obtains the query to search for keywords.
   923    *
   924    * @return an array consisting of the correctly optimized query to search the
   925    *         database with and an object containing the params to bound.
   926    */
   927   get _keywordQuery() {
   928     // The keyword is the first word in the search string, with the parameters
   929     // following it.
   930     let searchString = this._originalSearchString;
   931     let queryString = "";
   932     let queryIndex = searchString.indexOf(" ");
   933     if (queryIndex != -1) {
   934       queryString = searchString.substring(queryIndex + 1);
   935     }
   936     // We need to escape the parameters as if they were the query in a URL
   937     queryString = encodeURIComponent(queryString).replace("%20", "+", "g");
   939     // The first word could be a keyword, so that's what we'll search.
   940     let keyword = this._searchTokens[0];
   942     return [
   943       SQL_KEYWORD_QUERY,
   944       {
   945         keyword: keyword,
   946         query_string: queryString,
   947         query_type: QUERYTYPE_KEYWORD
   948       }
   949     ];
   950   },
   952   /**
   953    * Obtains the query to search for switch-to-tab entries.
   954    *
   955    * @return an array consisting of the correctly optimized query to search the
   956    *         database with and an object containing the params to bound.
   957    */
   958   get _switchToTabQuery() [
   959     SQL_SWITCHTAB_QUERY,
   960     {
   961       query_type: QUERYTYPE_FILTERED,
   962       matchBehavior: this._matchBehavior,
   963       searchBehavior: this._behavior,
   964       // We only want to search the tokens that we are left with - not the
   965       // original search string.
   966       searchString: this._searchTokens.join(" "),
   967       maxResults: Prefs.maxRichResults
   968     }
   969   ],
   971   /**
   972    * Obtains the query to search for adaptive results.
   973    *
   974    * @return an array consisting of the correctly optimized query to search the
   975    *         database with and an object containing the params to bound.
   976    */
   977   get _adaptiveQuery() [
   978     SQL_ADAPTIVE_QUERY,
   979     {
   980       parent: PlacesUtils.tagsFolderId,
   981       search_string: this._searchString,
   982       query_type: QUERYTYPE_FILTERED,
   983       matchBehavior: this._matchBehavior,
   984       searchBehavior: this._behavior
   985     }
   986   ],
   988   /**
   989    * Whether we should try to autoFill.
   990    */
   991   get _shouldAutofill() {
   992     // First of all, check for the autoFill pref.
   993     if (!Prefs.autofill)
   994       return false;
   996     // Then, we should not try to autofill if the behavior is not the default.
   997     // TODO (bug 751709): Ideally we should have a more fine-grained behavior
   998     // here, but for now it's enough to just check for default behavior.
   999     if (Prefs.defaultBehavior != DEFAULT_BEHAVIOR)
  1000       return false;
  1002     // Don't autoFill if the search term is recognized as a keyword, otherwise
  1003     // it will override default keywords behavior.  Note that keywords are
  1004     // hashed on first use, so while the first query may delay a little bit,
  1005     // next ones will just hit the memory hash.
  1006     if (this._searchString.length == 0 ||
  1007         PlacesUtils.bookmarks.getURIForKeyword(this._searchString)) {
  1008       return false;
  1011     // Don't try to autofill if the search term includes any whitespace.
  1012     // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
  1013     // tokenizer ends up trimming the search string and returning a value
  1014     // that doesn't match it, or is even shorter.
  1015     if (/\s/.test(this._searchString)) {
  1016       return false;
  1019     return true;
  1020   },
  1022   /**
  1023    * Obtains the query to search for autoFill host results.
  1025    * @return an array consisting of the correctly optimized query to search the
  1026    *         database with and an object containing the params to bound.
  1027    */
  1028   get _hostQuery() [
  1029     Prefs.autofillTyped ? SQL_TYPED_HOST_QUERY : SQL_TYPED_QUERY,
  1031       query_type: QUERYTYPE_AUTOFILL_HOST,
  1032       searchString: this._searchString.toLowerCase()
  1034   ],
  1036   /**
  1037    * Obtains the query to search for autoFill url results.
  1039    * @return an array consisting of the correctly optimized query to search the
  1040    *         database with and an object containing the params to bound.
  1041    */
  1042   get _urlQuery() [
  1043     Prefs.autofillTyped ? SQL_TYPED_HOST_QUERY : SQL_TYPED_QUERY,
  1045       query_type: QUERYTYPE_AUTOFILL_URL,
  1046       searchString: this._autofillUrlSearchString,
  1047       matchBehavior: MATCH_BEGINNING_CASE_SENSITIVE,
  1048       searchBehavior: Ci.mozIPlacesAutoComplete.BEHAVIOR_URL
  1050   ],
  1052  /**
  1053    * Notifies the listener about results.
  1055    * @param searchOngoing
  1056    *        Indicates whether the search is ongoing.
  1057    */
  1058   notifyResults: function (searchOngoing) {
  1059     let result = this._result;
  1060     let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
  1061     if (searchOngoing) {
  1062       resultCode += "_ONGOING";
  1064     result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
  1065     this._listener.onSearchResult(this._autocompleteSearch, result);
  1066   },
  1069 ////////////////////////////////////////////////////////////////////////////////
  1070 //// UnifiedComplete class
  1071 //// component @mozilla.org/autocomplete/search;1?name=unifiedcomplete
  1073 function UnifiedComplete() {
  1074   Services.obs.addObserver(this, TOPIC_SHUTDOWN, true);
  1077 UnifiedComplete.prototype = {
  1078   //////////////////////////////////////////////////////////////////////////////
  1079   //// nsIObserver
  1081   observe: function (subject, topic, data) {
  1082     if (topic === TOPIC_SHUTDOWN) {
  1083       this.ensureShutdown();
  1085   },
  1087   //////////////////////////////////////////////////////////////////////////////
  1088   //// Database handling
  1090   /**
  1091    * Promise resolved when the database initialization has completed, or null
  1092    * if it has never been requested.
  1093    */
  1094   _promiseDatabase: null,
  1096   /**
  1097    * Gets a Sqlite database handle.
  1099    * @return {Promise}
  1100    * @resolves to the Sqlite database handle (according to Sqlite.jsm).
  1101    * @rejects javascript exception.
  1102    */
  1103   getDatabaseHandle: function () {
  1104     if (Prefs.enabled && !this._promiseDatabase) {
  1105       this._promiseDatabase = Task.spawn(function* () {
  1106         let conn = yield Sqlite.cloneStorageConnection({
  1107           connection: PlacesUtils.history.DBConnection,
  1108           readOnly: true
  1109         });
  1111         // Autocomplete often fallbacks to a table scan due to lack of text
  1112         // indices.  A larger cache helps reducing IO and improving performance.
  1113         // The value used here is larger than the default Storage value defined
  1114         // as MAX_CACHE_SIZE_BYTES in storage/src/mozStorageConnection.cpp.
  1115         yield conn.execute("PRAGMA cache_size = -6144"); // 6MiB
  1117         yield SwitchToTabStorage.initDatabase(conn);
  1119         return conn;
  1120       }.bind(this)).then(null, Cu.reportError);
  1122     return this._promiseDatabase;
  1123   },
  1125   /**
  1126    * Used to stop running queries and close the database handle.
  1127    */
  1128   ensureShutdown: function () {
  1129     if (this._promiseDatabase) {
  1130       Task.spawn(function* () {
  1131         let conn = yield this.getDatabaseHandle();
  1132         SwitchToTabStorage.shutdown();
  1133         yield conn.close()
  1134       }.bind(this)).then(null, Cu.reportError);
  1135       this._promiseDatabase = null;
  1137   },
  1139   //////////////////////////////////////////////////////////////////////////////
  1140   //// mozIPlacesAutoComplete
  1142   registerOpenPage: function PAC_registerOpenPage(uri) {
  1143     SwitchToTabStorage.add(uri);
  1144   },
  1146   unregisterOpenPage: function PAC_unregisterOpenPage(uri) {
  1147     SwitchToTabStorage.delete(uri);
  1148   },
  1150   //////////////////////////////////////////////////////////////////////////////
  1151   //// nsIAutoCompleteSearch
  1153   startSearch: function (searchString, searchParam, previousResult, listener) {
  1154     // Stop the search in case the controller has not taken care of it.
  1155     if (this._currentSearch) {
  1156       this.stopSearch();
  1159     // Note: We don't use previousResult to make sure ordering of results are
  1160     //       consistent.  See bug 412730 for more details.
  1162     this._currentSearch = new Search(searchString, searchParam, listener,
  1163                                      this, this);
  1165     // If we are not enabled, we need to return now.  Notice we need an empty
  1166     // result regardless, so we still create the Search object.
  1167     if (!Prefs.enabled) {
  1168       this.finishSearch(true);
  1169       return;
  1172     let search = this._currentSearch;
  1173     this.getDatabaseHandle().then(conn => search.execute(conn))
  1174                             .then(() => {
  1175                               if (search == this._currentSearch) {
  1176                                 this.finishSearch(true);
  1178                             }, Cu.reportError);
  1179   },
  1181   stopSearch: function () {
  1182     if (this._currentSearch) {
  1183       this._currentSearch.cancel();
  1185     this.finishSearch();
  1186   },
  1188   /**
  1189    * Properly cleans up when searching is completed.
  1191    * @param notify [optional]
  1192    *        Indicates if we should notify the AutoComplete listener about our
  1193    *        results or not.
  1194    */
  1195   finishSearch: function (notify=false) {
  1196     // Notify about results if we are supposed to.
  1197     if (notify) {
  1198       this._currentSearch.notifyResults(false);
  1201     // Clear our state
  1202     TelemetryStopwatch.cancel(TELEMETRY_1ST_RESULT);
  1203     delete this._currentSearch;
  1204   },
  1206   //////////////////////////////////////////////////////////////////////////////
  1207   //// nsIAutoCompleteSimpleResultListener
  1209   onValueRemoved: function (result, spec, removeFromDB) {
  1210     if (removeFromDB) {
  1211       PlacesUtils.history.removePage(NetUtil.newURI(spec));
  1213   },
  1215   //////////////////////////////////////////////////////////////////////////////
  1216   //// nsIAutoCompleteSearchDescriptor
  1218   get searchType() Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE,
  1220   //////////////////////////////////////////////////////////////////////////////
  1221   //// nsISupports
  1223   classID: Components.ID("f964a319-397a-4d21-8be6-5cdd1ee3e3ae"),
  1225   _xpcom_factory: XPCOMUtils.generateSingletonFactory(UnifiedComplete),
  1227   QueryInterface: XPCOMUtils.generateQI([
  1228     Ci.nsIAutoCompleteSearch,
  1229     Ci.nsIAutoCompleteSimpleResultListener,
  1230     Ci.mozIPlacesAutoComplete,
  1231     Ci.nsIObserver,
  1232     Ci.nsISupportsWeakReference
  1233   ])
  1234 };
  1236 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UnifiedComplete]);

mercurial