michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- michael@0: * vim: sw=2 ts=2 sts=2 expandtab michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", michael@0: "resource://gre/modules/PlacesUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", michael@0: "resource://gre/modules/TelemetryStopwatch.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", michael@0: "resource://gre/modules/NetUtil.jsm"); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Constants michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: michael@0: // This SQL query fragment provides the following: michael@0: // - whether the entry is bookmarked (kQueryIndexBookmarked) michael@0: // - the bookmark title, if it is a bookmark (kQueryIndexBookmarkTitle) michael@0: // - the tags associated with a bookmarked entry (kQueryIndexTags) michael@0: const kBookTagSQLFragment = michael@0: "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked, " michael@0: + "( " michael@0: + "SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL " michael@0: + "ORDER BY lastModified DESC LIMIT 1 " michael@0: + ") AS btitle, " michael@0: + "( " michael@0: + "SELECT GROUP_CONCAT(t.title, ',') " michael@0: + "FROM moz_bookmarks b " michael@0: + "JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent " michael@0: + "WHERE b.fk = h.id " michael@0: + ") AS tags"; michael@0: michael@0: // observer topics michael@0: const kTopicShutdown = "places-shutdown"; michael@0: const kPrefChanged = "nsPref:changed"; michael@0: michael@0: // Match type constants. These indicate what type of search function we should michael@0: // be using. michael@0: const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE; michael@0: const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE; michael@0: const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY; michael@0: const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING; michael@0: const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE; michael@0: michael@0: // AutoComplete index constants. All AutoComplete queries will provide these michael@0: // columns in this order. michael@0: const kQueryIndexURL = 0; michael@0: const kQueryIndexTitle = 1; michael@0: const kQueryIndexFaviconURL = 2; michael@0: const kQueryIndexBookmarked = 3; michael@0: const kQueryIndexBookmarkTitle = 4; michael@0: const kQueryIndexTags = 5; michael@0: const kQueryIndexVisitCount = 6; michael@0: const kQueryIndexTyped = 7; michael@0: const kQueryIndexPlaceId = 8; michael@0: const kQueryIndexQueryType = 9; michael@0: const kQueryIndexOpenPageCount = 10; michael@0: michael@0: // AutoComplete query type constants. Describes the various types of queries michael@0: // that we can process. michael@0: const kQueryTypeKeyword = 0; michael@0: const kQueryTypeFiltered = 1; michael@0: michael@0: // This separator is used as an RTL-friendly way to split the title and tags. michael@0: // It can also be used by an nsIAutoCompleteResult consumer to re-split the michael@0: // "comment" back into the title and the tag. michael@0: const kTitleTagsSeparator = " \u2013 "; michael@0: michael@0: const kBrowserUrlbarBranch = "browser.urlbar."; michael@0: // Toggle autocomplete. michael@0: const kBrowserUrlbarAutocompleteEnabledPref = "autocomplete.enabled"; michael@0: // Toggle autoFill. michael@0: const kBrowserUrlbarAutofillPref = "autoFill"; michael@0: // Whether to search only typed entries. michael@0: const kBrowserUrlbarAutofillTypedPref = "autoFill.typed"; michael@0: michael@0: // The Telemetry histogram for urlInlineComplete query on domain michael@0: const DOMAIN_QUERY_TELEMETRY = "PLACES_AUTOCOMPLETE_URLINLINE_DOMAIN_QUERY_TIME_MS"; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Globals michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gTextURIService", michael@0: "@mozilla.org/intl/texttosuburi;1", michael@0: "nsITextToSubURI"); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Helpers michael@0: michael@0: /** michael@0: * Initializes our temporary table on a given database. michael@0: * michael@0: * @param aDatabase michael@0: * The mozIStorageConnection to set up the temp table on. michael@0: */ michael@0: function initTempTable(aDatabase) michael@0: { michael@0: // Note: this should be kept up-to-date with the definition in michael@0: // nsPlacesTables.h. michael@0: let stmt = aDatabase.createAsyncStatement( michael@0: "CREATE TEMP TABLE moz_openpages_temp ( " michael@0: + " url TEXT PRIMARY KEY " michael@0: + ", open_count INTEGER " michael@0: + ") " michael@0: ); michael@0: stmt.executeAsync(); michael@0: stmt.finalize(); michael@0: michael@0: // Note: this should be kept up-to-date with the definition in michael@0: // nsPlacesTriggers.h. michael@0: stmt = aDatabase.createAsyncStatement( michael@0: "CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger " michael@0: + "AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW " michael@0: + "WHEN NEW.open_count = 0 " michael@0: + "BEGIN " michael@0: + "DELETE FROM moz_openpages_temp " michael@0: + "WHERE url = NEW.url; " michael@0: + "END " michael@0: ); michael@0: stmt.executeAsync(); michael@0: stmt.finalize(); michael@0: } michael@0: michael@0: /** michael@0: * Used to unescape encoded URI strings, and drop information that we do not michael@0: * care about for searching. michael@0: * michael@0: * @param aURIString michael@0: * The text to unescape and modify. michael@0: * @return the modified uri. michael@0: */ michael@0: function fixupSearchText(aURIString) michael@0: { michael@0: let uri = stripPrefix(aURIString); michael@0: return gTextURIService.unEscapeURIForUI("UTF-8", uri); michael@0: } michael@0: michael@0: /** michael@0: * Strip prefixes from the URI that we don't care about for searching. michael@0: * michael@0: * @param aURIString michael@0: * The text to modify. michael@0: * @return the modified uri. michael@0: */ michael@0: function stripPrefix(aURIString) michael@0: { michael@0: let uri = aURIString; michael@0: michael@0: if (uri.indexOf("http://") == 0) { michael@0: uri = uri.slice(7); michael@0: } michael@0: else if (uri.indexOf("https://") == 0) { michael@0: uri = uri.slice(8); michael@0: } michael@0: else if (uri.indexOf("ftp://") == 0) { michael@0: uri = uri.slice(6); michael@0: } michael@0: michael@0: if (uri.indexOf("www.") == 0) { michael@0: uri = uri.slice(4); michael@0: } michael@0: return uri; michael@0: } michael@0: michael@0: /** michael@0: * safePrefGetter get the pref with typo safety. michael@0: * This will return the default value provided if no pref is set. michael@0: * michael@0: * @param aPrefBranch michael@0: * The nsIPrefBranch containing the required preference michael@0: * @param aName michael@0: * A preference name michael@0: * @param aDefault michael@0: * The preference's default value michael@0: * @return the preference value or provided default michael@0: */ michael@0: michael@0: function safePrefGetter(aPrefBranch, aName, aDefault) { michael@0: let types = { michael@0: boolean: "Bool", michael@0: number: "Int", michael@0: string: "Char" michael@0: }; michael@0: let type = types[typeof(aDefault)]; michael@0: if (!type) { michael@0: throw "Unknown type!"; michael@0: } michael@0: // If the pref isn't set, we want to use the default. michael@0: try { michael@0: return aPrefBranch["get" + type + "Pref"](aName); michael@0: } michael@0: catch (e) { michael@0: return aDefault; michael@0: } michael@0: } michael@0: michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// AutoCompleteStatementCallbackWrapper class michael@0: michael@0: /** michael@0: * Wraps a callback and ensures that handleCompletion is not dispatched if the michael@0: * query is no longer tracked. michael@0: * michael@0: * @param aAutocomplete michael@0: * A reference to a nsPlacesAutoComplete. michael@0: * @param aCallback michael@0: * A reference to a mozIStorageStatementCallback michael@0: * @param aDBConnection michael@0: * The database connection to execute the queries on. michael@0: */ michael@0: function AutoCompleteStatementCallbackWrapper(aAutocomplete, aCallback, michael@0: aDBConnection) michael@0: { michael@0: this._autocomplete = aAutocomplete; michael@0: this._callback = aCallback; michael@0: this._db = aDBConnection; michael@0: } michael@0: michael@0: AutoCompleteStatementCallbackWrapper.prototype = { michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// mozIStorageStatementCallback michael@0: michael@0: handleResult: function ACSCW_handleResult(aResultSet) michael@0: { michael@0: this._callback.handleResult.apply(this._callback, arguments); michael@0: }, michael@0: michael@0: handleError: function ACSCW_handleError(aError) michael@0: { michael@0: this._callback.handleError.apply(this._callback, arguments); michael@0: }, michael@0: michael@0: handleCompletion: function ACSCW_handleCompletion(aReason) michael@0: { michael@0: // Only dispatch handleCompletion if we are not done searching and are a michael@0: // pending search. michael@0: if (!this._autocomplete.isSearchComplete() && michael@0: this._autocomplete.isPendingSearch(this._handle)) { michael@0: this._callback.handleCompletion.apply(this._callback, arguments); michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// AutoCompleteStatementCallbackWrapper michael@0: michael@0: /** michael@0: * Executes the specified query asynchronously. This object will notify michael@0: * this._callback if we should notify (logic explained in handleCompletion). michael@0: * michael@0: * @param aQueries michael@0: * The queries to execute asynchronously. michael@0: * @return a mozIStoragePendingStatement that can be used to cancel the michael@0: * queries. michael@0: */ michael@0: executeAsync: function ACSCW_executeAsync(aQueries) michael@0: { michael@0: return this._handle = this._db.executeAsync(aQueries, aQueries.length, michael@0: this); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsISupports michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.mozIStorageStatementCallback, michael@0: ]) michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// nsPlacesAutoComplete class michael@0: //// @mozilla.org/autocomplete/search;1?name=history michael@0: michael@0: function nsPlacesAutoComplete() michael@0: { michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Shared Constants for Smart Getters michael@0: michael@0: // TODO bug 412736 in case of a frecency tie, break it with h.typed and michael@0: // h.visit_count which is better than nothing. This is slow, so not doing it michael@0: // yet... michael@0: const SQL_BASE = "SELECT h.url, h.title, f.url, " + kBookTagSQLFragment + ", " michael@0: + "h.visit_count, h.typed, h.id, :query_type, " michael@0: + "t.open_count " michael@0: + "FROM moz_places h " michael@0: + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id " michael@0: + "LEFT JOIN moz_openpages_temp t ON t.url = h.url " michael@0: + "WHERE h.frecency <> 0 " michael@0: + "AND AUTOCOMPLETE_MATCH(:searchString, h.url, " michael@0: + "IFNULL(btitle, h.title), tags, " michael@0: + "h.visit_count, h.typed, " michael@0: + "bookmarked, t.open_count, " michael@0: + ":matchBehavior, :searchBehavior) " michael@0: + "{ADDITIONAL_CONDITIONS} " michael@0: + "ORDER BY h.frecency DESC, h.id DESC " michael@0: + "LIMIT :maxResults"; michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Smart Getters michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "_db", function() { michael@0: // Get a cloned, read-only version of the database. We'll only ever write michael@0: // to our own in-memory temp table, and having a cloned copy means we do not michael@0: // run the risk of our queries taking longer due to the main database michael@0: // connection performing a long-running task. michael@0: let db = PlacesUtils.history.DBConnection.clone(true); michael@0: michael@0: // Autocomplete often fallbacks to a table scan due to lack of text indices. michael@0: // In such cases a larger cache helps reducing IO. The default Storage michael@0: // value is MAX_CACHE_SIZE_BYTES in storage/src/mozStorageConnection.cpp. michael@0: let stmt = db.createAsyncStatement("PRAGMA cache_size = -6144"); // 6MiB michael@0: stmt.executeAsync(); michael@0: stmt.finalize(); michael@0: michael@0: // Create our in-memory tables for tab tracking. michael@0: initTempTable(db); michael@0: michael@0: // Populate the table with current open pages cache contents. michael@0: if (this._openPagesCache.length > 0) { michael@0: // Avoid getter re-entrance from the _registerOpenPageQuery lazy getter. michael@0: let stmt = this._registerOpenPageQuery = michael@0: db.createAsyncStatement(this._registerOpenPageQuerySQL); michael@0: let params = stmt.newBindingParamsArray(); michael@0: for (let i = 0; i < this._openPagesCache.length; i++) { michael@0: let bp = params.newBindingParams(); michael@0: bp.bindByName("page_url", this._openPagesCache[i]); michael@0: params.addParams(bp); michael@0: } michael@0: stmt.bindParameters(params); michael@0: stmt.executeAsync(); michael@0: stmt.finalize(); michael@0: delete this._openPagesCache; michael@0: } michael@0: michael@0: return db; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "_defaultQuery", function() { michael@0: let replacementText = ""; michael@0: return this._db.createAsyncStatement( michael@0: SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g") michael@0: ); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "_historyQuery", function() { michael@0: // Enforce ignoring the visit_count index, since the frecency one is much michael@0: // faster in this case. ANALYZE helps the query planner to figure out the michael@0: // faster path, but it may not have run yet. michael@0: let replacementText = "AND +h.visit_count > 0"; michael@0: return this._db.createAsyncStatement( michael@0: SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g") michael@0: ); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "_bookmarkQuery", function() { michael@0: let replacementText = "AND bookmarked"; michael@0: return this._db.createAsyncStatement( michael@0: SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g") michael@0: ); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "_tagsQuery", function() { michael@0: let replacementText = "AND tags IS NOT NULL"; michael@0: return this._db.createAsyncStatement( michael@0: SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g") michael@0: ); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "_openPagesQuery", function() { michael@0: return this._db.createAsyncStatement( michael@0: "SELECT t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL, " michael@0: + ":query_type, t.open_count, NULL " michael@0: + "FROM moz_openpages_temp t " michael@0: + "LEFT JOIN moz_places h ON h.url = t.url " michael@0: + "WHERE h.id IS NULL " michael@0: + "AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL, " michael@0: + "NULL, NULL, NULL, t.open_count, " michael@0: + ":matchBehavior, :searchBehavior) " michael@0: + "ORDER BY t.ROWID DESC " michael@0: + "LIMIT :maxResults " michael@0: ); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "_typedQuery", function() { michael@0: let replacementText = "AND h.typed = 1"; michael@0: return this._db.createAsyncStatement( michael@0: SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g") michael@0: ); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "_adaptiveQuery", function() { michael@0: return this._db.createAsyncStatement( michael@0: "/* do not warn (bug 487789) */ " michael@0: + "SELECT h.url, h.title, f.url, " + kBookTagSQLFragment + ", " michael@0: + "h.visit_count, h.typed, h.id, :query_type, t.open_count " michael@0: + "FROM ( " michael@0: + "SELECT ROUND( " michael@0: + "MAX(use_count) * (1 + (input = :search_string)), 1 " michael@0: + ") AS rank, place_id " michael@0: + "FROM moz_inputhistory " michael@0: + "WHERE input BETWEEN :search_string AND :search_string || X'FFFF' " michael@0: + "GROUP BY place_id " michael@0: + ") AS i " michael@0: + "JOIN moz_places h ON h.id = i.place_id " michael@0: + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id " michael@0: + "LEFT JOIN moz_openpages_temp t ON t.url = h.url " michael@0: + "WHERE AUTOCOMPLETE_MATCH(NULL, h.url, " michael@0: + "IFNULL(btitle, h.title), tags, " michael@0: + "h.visit_count, h.typed, bookmarked, " michael@0: + "t.open_count, " michael@0: + ":matchBehavior, :searchBehavior) " michael@0: + "ORDER BY rank DESC, h.frecency DESC " michael@0: ); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "_keywordQuery", function() { michael@0: return this._db.createAsyncStatement( michael@0: "/* do not warn (bug 487787) */ " michael@0: + "SELECT " michael@0: + "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk) " michael@0: + "AS search_url, h.title, " michael@0: + "IFNULL(f.url, (SELECT f.url " michael@0: + "FROM moz_places " michael@0: + "JOIN moz_favicons f ON f.id = favicon_id " michael@0: + "WHERE rev_host = (SELECT rev_host FROM moz_places WHERE id = b.fk) " michael@0: + "ORDER BY frecency DESC " michael@0: + "LIMIT 1) " michael@0: + "), 1, b.title, NULL, h.visit_count, h.typed, IFNULL(h.id, b.fk), " michael@0: + ":query_type, t.open_count " michael@0: + "FROM moz_keywords k " michael@0: + "JOIN moz_bookmarks b ON b.keyword_id = k.id " michael@0: + "LEFT JOIN moz_places h ON h.url = search_url " michael@0: + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id " michael@0: + "LEFT JOIN moz_openpages_temp t ON t.url = search_url " michael@0: + "WHERE LOWER(k.keyword) = LOWER(:keyword) " michael@0: + "ORDER BY h.frecency DESC " michael@0: ); michael@0: }); michael@0: michael@0: this._registerOpenPageQuerySQL = "INSERT OR REPLACE INTO moz_openpages_temp " michael@0: + "(url, open_count) " michael@0: + "VALUES (:page_url, " michael@0: + "IFNULL(" michael@0: + "(" michael@0: + "SELECT open_count + 1 " michael@0: + "FROM moz_openpages_temp " michael@0: + "WHERE url = :page_url " michael@0: + "), " michael@0: + "1" michael@0: + ")" michael@0: + ")"; michael@0: XPCOMUtils.defineLazyGetter(this, "_registerOpenPageQuery", function() { michael@0: return this._db.createAsyncStatement(this._registerOpenPageQuerySQL); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "_unregisterOpenPageQuery", function() { michael@0: return this._db.createAsyncStatement( michael@0: "UPDATE moz_openpages_temp " michael@0: + "SET open_count = open_count - 1 " michael@0: + "WHERE url = :page_url" michael@0: ); michael@0: }); michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Initialization michael@0: michael@0: // load preferences michael@0: this._prefs = Cc["@mozilla.org/preferences-service;1"]. michael@0: getService(Ci.nsIPrefService). michael@0: getBranch(kBrowserUrlbarBranch); michael@0: this._loadPrefs(true); michael@0: michael@0: // register observers michael@0: this._os = Cc["@mozilla.org/observer-service;1"]. michael@0: getService(Ci.nsIObserverService); michael@0: this._os.addObserver(this, kTopicShutdown, false); michael@0: michael@0: } michael@0: michael@0: nsPlacesAutoComplete.prototype = { michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsIAutoCompleteSearch michael@0: michael@0: startSearch: function PAC_startSearch(aSearchString, aSearchParam, michael@0: aPreviousResult, aListener) michael@0: { michael@0: // Stop the search in case the controller has not taken care of it. michael@0: this.stopSearch(); michael@0: michael@0: // Note: We don't use aPreviousResult to make sure ordering of results are michael@0: // consistent. See bug 412730 for more details. michael@0: michael@0: // We want to store the original string with no leading or trailing michael@0: // whitespace for case sensitive searches. michael@0: this._originalSearchString = aSearchString.trim(); michael@0: michael@0: this._currentSearchString = michael@0: fixupSearchText(this._originalSearchString.toLowerCase()); michael@0: michael@0: let searchParamParts = aSearchParam.split(" "); michael@0: this._enableActions = searchParamParts.indexOf("enable-actions") != -1; michael@0: michael@0: this._listener = aListener; michael@0: let result = Cc["@mozilla.org/autocomplete/simple-result;1"]. michael@0: createInstance(Ci.nsIAutoCompleteSimpleResult); michael@0: result.setSearchString(aSearchString); michael@0: result.setListener(this); michael@0: this._result = result; michael@0: michael@0: // If we are not enabled, we need to return now. michael@0: if (!this._enabled) { michael@0: this._finishSearch(true); michael@0: return; michael@0: } michael@0: michael@0: // Reset our search behavior to the default. michael@0: if (this._currentSearchString) { michael@0: this._behavior = this._defaultBehavior; michael@0: } michael@0: else { michael@0: this._behavior = this._emptySearchDefaultBehavior; michael@0: } michael@0: // For any given search, we run up to four queries: michael@0: // 1) keywords (this._keywordQuery) michael@0: // 2) adaptive learning (this._adaptiveQuery) michael@0: // 3) open pages not supported by history (this._openPagesQuery) michael@0: // 4) query from this._getSearch michael@0: // (1) only gets ran if we get any filtered tokens from this._getSearch, michael@0: // since if there are no tokens, there is nothing to match, so there is no michael@0: // reason to run the query). michael@0: let {query, tokens} = michael@0: this._getSearch(this._getUnfilteredSearchTokens(this._currentSearchString)); michael@0: let queries = tokens.length ? michael@0: [this._getBoundKeywordQuery(tokens), this._getBoundAdaptiveQuery(), this._getBoundOpenPagesQuery(tokens), query] : michael@0: [this._getBoundAdaptiveQuery(), this._getBoundOpenPagesQuery(tokens), query]; michael@0: michael@0: // Start executing our queries. michael@0: this._telemetryStartTime = Date.now(); michael@0: this._executeQueries(queries); michael@0: michael@0: // Set up our persistent state for the duration of the search. michael@0: this._searchTokens = tokens; michael@0: this._usedPlaces = {}; michael@0: }, michael@0: michael@0: stopSearch: function PAC_stopSearch() michael@0: { michael@0: // We need to cancel our searches so we do not get any [more] results. michael@0: // However, it's possible we haven't actually started any searches, so this michael@0: // method may throw because this._pendingQuery may be undefined. michael@0: if (this._pendingQuery) { michael@0: this._stopActiveQuery(); michael@0: } michael@0: michael@0: this._finishSearch(false); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsIAutoCompleteSimpleResultListener michael@0: michael@0: onValueRemoved: function PAC_onValueRemoved(aResult, aURISpec, aRemoveFromDB) michael@0: { michael@0: if (aRemoveFromDB) { michael@0: PlacesUtils.history.removePage(NetUtil.newURI(aURISpec)); michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// mozIPlacesAutoComplete michael@0: michael@0: // If the connection has not yet been started, use this local cache. This michael@0: // prevents autocomplete from initing the database till the first search. michael@0: _openPagesCache: [], michael@0: registerOpenPage: function PAC_registerOpenPage(aURI) michael@0: { michael@0: if (!this._databaseInitialized) { michael@0: this._openPagesCache.push(aURI.spec); michael@0: return; michael@0: } michael@0: michael@0: let stmt = this._registerOpenPageQuery; michael@0: stmt.params.page_url = aURI.spec; michael@0: stmt.executeAsync(); michael@0: }, michael@0: michael@0: unregisterOpenPage: function PAC_unregisterOpenPage(aURI) michael@0: { michael@0: if (!this._databaseInitialized) { michael@0: let index = this._openPagesCache.indexOf(aURI.spec); michael@0: if (index != -1) { michael@0: this._openPagesCache.splice(index, 1); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: let stmt = this._unregisterOpenPageQuery; michael@0: stmt.params.page_url = aURI.spec; michael@0: stmt.executeAsync(); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// mozIStorageStatementCallback michael@0: michael@0: handleResult: function PAC_handleResult(aResultSet) michael@0: { michael@0: let row, haveMatches = false; michael@0: while ((row = aResultSet.getNextRow())) { michael@0: let match = this._processRow(row); michael@0: haveMatches = haveMatches || match; michael@0: michael@0: if (this._result.matchCount == this._maxRichResults) { michael@0: // We have enough results, so stop running our search. michael@0: this._stopActiveQuery(); michael@0: michael@0: // And finish our search. michael@0: this._finishSearch(true); michael@0: return; michael@0: } michael@0: michael@0: } michael@0: michael@0: // Notify about results if we've gotten them. michael@0: if (haveMatches) { michael@0: this._notifyResults(true); michael@0: } michael@0: }, michael@0: michael@0: handleError: function PAC_handleError(aError) michael@0: { michael@0: Components.utils.reportError("Places AutoComplete: An async statement encountered an " + michael@0: "error: " + aError.result + ", '" + aError.message + "'"); michael@0: }, michael@0: michael@0: handleCompletion: function PAC_handleCompletion(aReason) michael@0: { michael@0: // If we have already finished our search, we should bail out early. michael@0: if (this.isSearchComplete()) { michael@0: return; michael@0: } michael@0: michael@0: // If we do not have enough results, and our match type is michael@0: // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more michael@0: // results. michael@0: if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE && michael@0: this._result.matchCount < this._maxRichResults && !this._secondPass) { michael@0: this._secondPass = true; michael@0: let queries = [ michael@0: this._getBoundAdaptiveQuery(MATCH_ANYWHERE), michael@0: this._getBoundSearchQuery(MATCH_ANYWHERE, this._searchTokens), michael@0: ]; michael@0: this._executeQueries(queries); michael@0: return; michael@0: } michael@0: michael@0: this._finishSearch(true); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsIObserver michael@0: michael@0: observe: function PAC_observe(aSubject, aTopic, aData) michael@0: { michael@0: if (aTopic == kTopicShutdown) { michael@0: this._os.removeObserver(this, kTopicShutdown); michael@0: michael@0: // Remove our preference observer. michael@0: this._prefs.removeObserver("", this); michael@0: delete this._prefs; michael@0: michael@0: // Finalize the statements that we have used. michael@0: let stmts = [ michael@0: "_defaultQuery", michael@0: "_historyQuery", michael@0: "_bookmarkQuery", michael@0: "_tagsQuery", michael@0: "_openPagesQuery", michael@0: "_typedQuery", michael@0: "_adaptiveQuery", michael@0: "_keywordQuery", michael@0: "_registerOpenPageQuery", michael@0: "_unregisterOpenPageQuery", michael@0: ]; michael@0: for (let i = 0; i < stmts.length; i++) { michael@0: // We do not want to create any query we haven't already created, so michael@0: // see if it is a getter first. michael@0: if (Object.getOwnPropertyDescriptor(this, stmts[i]).value !== undefined) { michael@0: this[stmts[i]].finalize(); michael@0: } michael@0: } michael@0: michael@0: if (this._databaseInitialized) { michael@0: this._db.asyncClose(); michael@0: } michael@0: } michael@0: else if (aTopic == kPrefChanged) { michael@0: this._loadPrefs(); michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsPlacesAutoComplete michael@0: michael@0: get _databaseInitialized() michael@0: Object.getOwnPropertyDescriptor(this, "_db").value !== undefined, michael@0: michael@0: /** michael@0: * Generates the tokens used in searching from a given string. michael@0: * michael@0: * @param aSearchString michael@0: * The string to generate tokens from. michael@0: * @return an array of tokens. michael@0: */ michael@0: _getUnfilteredSearchTokens: function PAC_unfilteredSearchTokens(aSearchString) michael@0: { michael@0: // Calling split on an empty string will return an array containing one michael@0: // empty string. We don't want that, as it'll break our logic, so return an michael@0: // empty array then. michael@0: return aSearchString.length ? aSearchString.split(" ") : []; michael@0: }, michael@0: michael@0: /** michael@0: * Properly cleans up when searching is completed. michael@0: * michael@0: * @param aNotify michael@0: * Indicates if we should notify the AutoComplete listener about our michael@0: * results or not. michael@0: */ michael@0: _finishSearch: function PAC_finishSearch(aNotify) michael@0: { michael@0: // Notify about results if we are supposed to. michael@0: if (aNotify) { michael@0: this._notifyResults(false); michael@0: } michael@0: michael@0: // Clear our state michael@0: delete this._originalSearchString; michael@0: delete this._currentSearchString; michael@0: delete this._strippedPrefix; michael@0: delete this._searchTokens; michael@0: delete this._listener; michael@0: delete this._result; michael@0: delete this._usedPlaces; michael@0: delete this._pendingQuery; michael@0: this._secondPass = false; michael@0: this._enableActions = false; michael@0: }, michael@0: michael@0: /** michael@0: * Executes the given queries asynchronously. michael@0: * michael@0: * @param aQueries michael@0: * The queries to execute. michael@0: */ michael@0: _executeQueries: function PAC_executeQueries(aQueries) michael@0: { michael@0: // Because we might get a handleCompletion for canceled queries, we want to michael@0: // filter out queries we no longer care about (described in the michael@0: // handleCompletion implementation of AutoCompleteStatementCallbackWrapper). michael@0: michael@0: // Create our wrapper object and execute the queries. michael@0: let wrapper = new AutoCompleteStatementCallbackWrapper(this, this, this._db); michael@0: this._pendingQuery = wrapper.executeAsync(aQueries); michael@0: }, michael@0: michael@0: /** michael@0: * Stops executing our active query. michael@0: */ michael@0: _stopActiveQuery: function PAC_stopActiveQuery() michael@0: { michael@0: this._pendingQuery.cancel(); michael@0: delete this._pendingQuery; michael@0: }, michael@0: michael@0: /** michael@0: * Notifies the listener about results. michael@0: * michael@0: * @param aSearchOngoing michael@0: * Indicates if the search is ongoing or not. michael@0: */ michael@0: _notifyResults: function PAC_notifyResults(aSearchOngoing) michael@0: { michael@0: let result = this._result; michael@0: let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH"; michael@0: if (aSearchOngoing) { michael@0: resultCode += "_ONGOING"; michael@0: } michael@0: result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]); michael@0: this._listener.onSearchResult(this, result); michael@0: if (this._telemetryStartTime) { michael@0: let elapsed = Date.now() - this._telemetryStartTime; michael@0: if (elapsed > 50) { michael@0: try { michael@0: Services.telemetry michael@0: .getHistogramById("PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS") michael@0: .add(elapsed); michael@0: } catch (ex) { michael@0: Components.utils.reportError("Unable to report telemetry."); michael@0: } michael@0: } michael@0: this._telemetryStartTime = null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Loads the preferences that we care about. michael@0: * michael@0: * @param [optional] aRegisterObserver michael@0: * Indicates if the preference observer should be added or not. The michael@0: * default value is false. michael@0: */ michael@0: _loadPrefs: function PAC_loadPrefs(aRegisterObserver) michael@0: { michael@0: this._enabled = safePrefGetter(this._prefs, michael@0: kBrowserUrlbarAutocompleteEnabledPref, michael@0: true); michael@0: this._matchBehavior = safePrefGetter(this._prefs, michael@0: "matchBehavior", michael@0: MATCH_BOUNDARY_ANYWHERE); michael@0: this._filterJavaScript = safePrefGetter(this._prefs, "filter.javascript", true); michael@0: this._maxRichResults = safePrefGetter(this._prefs, "maxRichResults", 25); michael@0: this._restrictHistoryToken = safePrefGetter(this._prefs, michael@0: "restrict.history", "^"); michael@0: this._restrictBookmarkToken = safePrefGetter(this._prefs, michael@0: "restrict.bookmark", "*"); michael@0: this._restrictTypedToken = safePrefGetter(this._prefs, "restrict.typed", "~"); michael@0: this._restrictTagToken = safePrefGetter(this._prefs, "restrict.tag", "+"); michael@0: this._restrictOpenPageToken = safePrefGetter(this._prefs, michael@0: "restrict.openpage", "%"); michael@0: this._matchTitleToken = safePrefGetter(this._prefs, "match.title", "#"); michael@0: this._matchURLToken = safePrefGetter(this._prefs, "match.url", "@"); michael@0: this._defaultBehavior = safePrefGetter(this._prefs, "default.behavior", 0); michael@0: // Further restrictions to apply for "empty searches" (i.e. searches for ""). michael@0: this._emptySearchDefaultBehavior = michael@0: this._defaultBehavior | michael@0: safePrefGetter(this._prefs, "default.behavior.emptyRestriction", michael@0: Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY | michael@0: Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED); michael@0: michael@0: // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE. michael@0: if (this._matchBehavior != MATCH_ANYWHERE && michael@0: this._matchBehavior != MATCH_BOUNDARY && michael@0: this._matchBehavior != MATCH_BEGINNING) { michael@0: this._matchBehavior = MATCH_BOUNDARY_ANYWHERE; michael@0: } michael@0: // register observer michael@0: if (aRegisterObserver) { michael@0: this._prefs.addObserver("", this, false); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Given an array of tokens, this function determines which query should be michael@0: * ran. It also removes any special search tokens. michael@0: * michael@0: * @param aTokens michael@0: * An array of search tokens. michael@0: * @return an object with two properties: michael@0: * query: the correctly optimized, bound query to search the database michael@0: * with. michael@0: * tokens: the filtered list of tokens to search with. michael@0: */ michael@0: _getSearch: function PAC_getSearch(aTokens) michael@0: { michael@0: // Set the proper behavior so our call to _getBoundSearchQuery gives us the michael@0: // correct query. michael@0: for (let i = aTokens.length - 1; i >= 0; i--) { michael@0: switch (aTokens[i]) { michael@0: case this._restrictHistoryToken: michael@0: this._setBehavior("history"); michael@0: break; michael@0: case this._restrictBookmarkToken: michael@0: this._setBehavior("bookmark"); michael@0: break; michael@0: case this._restrictTagToken: michael@0: this._setBehavior("tag"); michael@0: break; michael@0: case this._restrictOpenPageToken: michael@0: if (!this._enableActions) { michael@0: continue; michael@0: } michael@0: this._setBehavior("openpage"); michael@0: break; michael@0: case this._matchTitleToken: michael@0: this._setBehavior("title"); michael@0: break; michael@0: case this._matchURLToken: michael@0: this._setBehavior("url"); michael@0: break; michael@0: case this._restrictTypedToken: michael@0: this._setBehavior("typed"); michael@0: break; michael@0: default: michael@0: // We do not want to remove the token if we did not match. michael@0: continue; michael@0: }; michael@0: michael@0: aTokens.splice(i, 1); michael@0: } michael@0: michael@0: // Set the right JavaScript behavior based on our preference. Note that the michael@0: // preference is whether or not we should filter JavaScript, and the michael@0: // behavior is if we should search it or not. michael@0: if (!this._filterJavaScript) { michael@0: this._setBehavior("javascript"); michael@0: } michael@0: michael@0: return { michael@0: query: this._getBoundSearchQuery(this._matchBehavior, aTokens), michael@0: tokens: aTokens michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Obtains the search query to be used based on the previously set search michael@0: * behaviors (accessed by this._hasBehavior). The query is bound and ready to michael@0: * execute. michael@0: * michael@0: * @param aMatchBehavior michael@0: * How this query should match its tokens to the search string. michael@0: * @param aTokens michael@0: * An array of search tokens. michael@0: * @return the correctly optimized query to search the database with and the michael@0: * new list of tokens to search with. The query has all the needed michael@0: * parameters bound, so consumers can execute it without doing any michael@0: * additional work. michael@0: */ michael@0: _getBoundSearchQuery: function PAC_getBoundSearchQuery(aMatchBehavior, michael@0: aTokens) michael@0: { michael@0: // We use more optimized queries for restricted searches, so we will always michael@0: // return the most restrictive one to the least restrictive one if more than michael@0: // one token is found. michael@0: // Note: "openpages" behavior is supported by the default query. michael@0: // _openPagesQuery instead returns only pages not supported by michael@0: // history and it is always executed. michael@0: let query = this._hasBehavior("tag") ? this._tagsQuery : michael@0: this._hasBehavior("bookmark") ? this._bookmarkQuery : michael@0: this._hasBehavior("typed") ? this._typedQuery : michael@0: this._hasBehavior("history") ? this._historyQuery : michael@0: this._defaultQuery; michael@0: michael@0: // Bind the needed parameters to the query so consumers can use it. michael@0: let (params = query.params) { michael@0: params.parent = PlacesUtils.tagsFolderId; michael@0: params.query_type = kQueryTypeFiltered; michael@0: params.matchBehavior = aMatchBehavior; michael@0: params.searchBehavior = this._behavior; michael@0: michael@0: // We only want to search the tokens that we are left with - not the michael@0: // original search string. michael@0: params.searchString = aTokens.join(" "); michael@0: michael@0: // Limit the query to the the maximum number of desired results. michael@0: // This way we can avoid doing more work than needed. michael@0: params.maxResults = this._maxRichResults; michael@0: } michael@0: michael@0: return query; michael@0: }, michael@0: michael@0: _getBoundOpenPagesQuery: function PAC_getBoundOpenPagesQuery(aTokens) michael@0: { michael@0: let query = this._openPagesQuery; michael@0: michael@0: // Bind the needed parameters to the query so consumers can use it. michael@0: let (params = query.params) { michael@0: params.query_type = kQueryTypeFiltered; michael@0: params.matchBehavior = this._matchBehavior; michael@0: params.searchBehavior = this._behavior; michael@0: // We only want to search the tokens that we are left with - not the michael@0: // original search string. michael@0: params.searchString = aTokens.join(" "); michael@0: params.maxResults = this._maxRichResults; michael@0: } michael@0: michael@0: return query; michael@0: }, michael@0: michael@0: /** michael@0: * Obtains the keyword query with the properly bound parameters. michael@0: * michael@0: * @param aTokens michael@0: * The array of search tokens to check against. michael@0: * @return the bound keyword query. michael@0: */ michael@0: _getBoundKeywordQuery: function PAC_getBoundKeywordQuery(aTokens) michael@0: { michael@0: // The keyword is the first word in the search string, with the parameters michael@0: // following it. michael@0: let searchString = this._originalSearchString; michael@0: let queryString = ""; michael@0: let queryIndex = searchString.indexOf(" "); michael@0: if (queryIndex != -1) { michael@0: queryString = searchString.substring(queryIndex + 1); michael@0: } michael@0: // We need to escape the parameters as if they were the query in a URL michael@0: queryString = encodeURIComponent(queryString).replace("%20", "+", "g"); michael@0: michael@0: // The first word could be a keyword, so that's what we'll search. michael@0: let keyword = aTokens[0]; michael@0: michael@0: let query = this._keywordQuery; michael@0: let (params = query.params) { michael@0: params.keyword = keyword; michael@0: params.query_string = queryString; michael@0: params.query_type = kQueryTypeKeyword; michael@0: } michael@0: michael@0: return query; michael@0: }, michael@0: michael@0: /** michael@0: * Obtains the adaptive query with the properly bound parameters. michael@0: * michael@0: * @return the bound adaptive query. michael@0: */ michael@0: _getBoundAdaptiveQuery: function PAC_getBoundAdaptiveQuery(aMatchBehavior) michael@0: { michael@0: // If we were not given a match behavior, use the stored match behavior. michael@0: if (arguments.length == 0) { michael@0: aMatchBehavior = this._matchBehavior; michael@0: } michael@0: michael@0: let query = this._adaptiveQuery; michael@0: let (params = query.params) { michael@0: params.parent = PlacesUtils.tagsFolderId; michael@0: params.search_string = this._currentSearchString; michael@0: params.query_type = kQueryTypeFiltered; michael@0: params.matchBehavior = aMatchBehavior; michael@0: params.searchBehavior = this._behavior; michael@0: } michael@0: michael@0: return query; michael@0: }, michael@0: michael@0: /** michael@0: * Processes a mozIStorageRow to generate the proper data for the AutoComplete michael@0: * result. This will add an entry to the current result if it matches the michael@0: * criteria. michael@0: * michael@0: * @param aRow michael@0: * The row to process. michael@0: * @return true if the row is accepted, and false if not. michael@0: */ michael@0: _processRow: function PAC_processRow(aRow) michael@0: { michael@0: // Before we do any work, make sure this entry isn't already in our results. michael@0: let entryId = aRow.getResultByIndex(kQueryIndexPlaceId); michael@0: let escapedEntryURL = aRow.getResultByIndex(kQueryIndexURL); michael@0: let openPageCount = aRow.getResultByIndex(kQueryIndexOpenPageCount) || 0; michael@0: michael@0: // If actions are enabled and the page is open, add only the switch-to-tab michael@0: // result. Otherwise, add the normal result. michael@0: let [url, action] = this._enableActions && openPageCount > 0 ? michael@0: ["moz-action:switchtab," + escapedEntryURL, "action "] : michael@0: [escapedEntryURL, ""]; michael@0: michael@0: if (this._inResults(entryId, url)) { michael@0: return false; michael@0: } michael@0: michael@0: let entryTitle = aRow.getResultByIndex(kQueryIndexTitle) || ""; michael@0: let entryFavicon = aRow.getResultByIndex(kQueryIndexFaviconURL) || ""; michael@0: let entryBookmarked = aRow.getResultByIndex(kQueryIndexBookmarked); michael@0: let entryBookmarkTitle = entryBookmarked ? michael@0: aRow.getResultByIndex(kQueryIndexBookmarkTitle) : null; michael@0: let entryTags = aRow.getResultByIndex(kQueryIndexTags) || ""; michael@0: michael@0: // Always prefer the bookmark title unless it is empty michael@0: let title = entryBookmarkTitle || entryTitle; michael@0: michael@0: let style; michael@0: if (aRow.getResultByIndex(kQueryIndexQueryType) == kQueryTypeKeyword) { michael@0: // If we do not have a title, then we must have a keyword, so let the UI michael@0: // know it is a keyword. Otherwise, we found an exact page match, so just michael@0: // show the page like a regular result. Because the page title is likely michael@0: // going to be more specific than the bookmark title (keyword title). michael@0: if (!entryTitle) { michael@0: style = "keyword"; michael@0: } michael@0: else { michael@0: title = entryTitle; michael@0: } michael@0: } michael@0: michael@0: // We will always prefer to show tags if we have them. michael@0: let showTags = !!entryTags; michael@0: michael@0: // However, we'll act as if a page is not bookmarked or tagged if the user michael@0: // only wants only history and not bookmarks or tags. michael@0: if (this._hasBehavior("history") && michael@0: !(this._hasBehavior("bookmark") || this._hasBehavior("tag"))) { michael@0: showTags = false; michael@0: style = "favicon"; michael@0: } michael@0: michael@0: // If we have tags and should show them, we need to add them to the title. michael@0: if (showTags) { michael@0: title += kTitleTagsSeparator + entryTags; michael@0: } michael@0: // We have to determine the right style to display. Tags show the tag icon, michael@0: // bookmarks get the bookmark icon, and keywords get the keyword icon. If michael@0: // the result does not fall into any of those, it just gets the favicon. michael@0: if (!style) { michael@0: // It is possible that we already have a style set (from a keyword michael@0: // search or because of the user's preferences), so only set it if we michael@0: // haven't already done so. michael@0: if (showTags) { michael@0: style = "tag"; michael@0: } michael@0: else if (entryBookmarked) { michael@0: style = "bookmark"; michael@0: } michael@0: else { michael@0: style = "favicon"; michael@0: } michael@0: } michael@0: michael@0: this._addToResults(entryId, url, title, entryFavicon, action + style); michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Checks to see if the given place has already been added to the results. michael@0: * michael@0: * @param aPlaceId michael@0: * The place id to check for, may be null. michael@0: * @param aUrl michael@0: * The url to check for. michael@0: * @return true if the place has been added, false otherwise. michael@0: * michael@0: * @note Must check both the id and the url for a negative match, since michael@0: * autocomplete may run in the middle of a new page addition. In such michael@0: * a case the switch-to-tab query would hash the page by url, then a michael@0: * next query, running after the page addition, would hash it by id. michael@0: * It's not possible to just rely on url though, since keywords michael@0: * dynamically modify the url to include their search string. michael@0: */ michael@0: _inResults: function PAC_inResults(aPlaceId, aUrl) michael@0: { michael@0: if (aPlaceId && aPlaceId in this._usedPlaces) { michael@0: return true; michael@0: } michael@0: return aUrl in this._usedPlaces; michael@0: }, michael@0: michael@0: /** michael@0: * Adds a result to the AutoComplete results. Also tracks that we've added michael@0: * this place_id into the result set. michael@0: * michael@0: * @param aPlaceId michael@0: * The place_id of the item to be added to the result set. This is michael@0: * used by _inResults. michael@0: * @param aURISpec michael@0: * The URI spec for the entry. michael@0: * @param aTitle michael@0: * The title to give the entry. michael@0: * @param aFaviconSpec michael@0: * The favicon to give to the entry. michael@0: * @param aStyle michael@0: * Indicates how the entry should be styled when displayed. michael@0: */ michael@0: _addToResults: function PAC_addToResults(aPlaceId, aURISpec, aTitle, michael@0: aFaviconSpec, aStyle) michael@0: { michael@0: // Add this to our internal tracker to ensure duplicates do not end up in michael@0: // the result. _usedPlaces is an Object that is being used as a set. michael@0: // Not all entries have a place id, thus we fallback to the url for them. michael@0: // We cannot use only the url since keywords entries are modified to michael@0: // include the search string, and would be returned multiple times. Ids michael@0: // are faster too. michael@0: this._usedPlaces[aPlaceId || aURISpec] = true; michael@0: michael@0: // Obtain the favicon for this URI. michael@0: let favicon; michael@0: if (aFaviconSpec) { michael@0: let uri = NetUtil.newURI(aFaviconSpec); michael@0: favicon = PlacesUtils.favicons.getFaviconLinkForIcon(uri).spec; michael@0: } michael@0: favicon = favicon || PlacesUtils.favicons.defaultFavicon.spec; michael@0: michael@0: this._result.appendMatch(aURISpec, aTitle, favicon, aStyle); michael@0: }, michael@0: michael@0: /** michael@0: * Determines if the specified AutoComplete behavior is set. michael@0: * michael@0: * @param aType michael@0: * The behavior type to test for. michael@0: * @return true if the behavior is set, false otherwise. michael@0: */ michael@0: _hasBehavior: function PAC_hasBehavior(aType) michael@0: { michael@0: return (this._behavior & michael@0: Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()]); michael@0: }, michael@0: michael@0: /** michael@0: * Enables the desired AutoComplete behavior. michael@0: * michael@0: * @param aType michael@0: * The behavior type to set. michael@0: */ michael@0: _setBehavior: function PAC_setBehavior(aType) michael@0: { michael@0: this._behavior |= michael@0: Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()]; michael@0: }, michael@0: michael@0: /** michael@0: * Determines if we are done searching or not. michael@0: * michael@0: * @return true if we have completed searching, false otherwise. michael@0: */ michael@0: isSearchComplete: function PAC_isSearchComplete() michael@0: { michael@0: // If _pendingQuery is null, we should no longer do any work since we have michael@0: // already called _finishSearch. This means we completed our search. michael@0: return this._pendingQuery == null; michael@0: }, michael@0: michael@0: /** michael@0: * Determines if the given handle of a pending statement is a pending search michael@0: * or not. michael@0: * michael@0: * @param aHandle michael@0: * A mozIStoragePendingStatement to check and see if we are waiting for michael@0: * results from it still. michael@0: * @return true if it is a pending query, false otherwise. michael@0: */ michael@0: isPendingSearch: function PAC_isPendingSearch(aHandle) michael@0: { michael@0: return this._pendingQuery == aHandle; michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsISupports michael@0: michael@0: classID: Components.ID("d0272978-beab-4adc-a3d4-04b76acfa4e7"), michael@0: michael@0: _xpcom_factory: XPCOMUtils.generateSingletonFactory(nsPlacesAutoComplete), michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsIAutoCompleteSearch, michael@0: Ci.nsIAutoCompleteSimpleResultListener, michael@0: Ci.mozIPlacesAutoComplete, michael@0: Ci.mozIStorageStatementCallback, michael@0: Ci.nsIObserver, michael@0: Ci.nsISupportsWeakReference, michael@0: ]) michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// urlInlineComplete class michael@0: //// component @mozilla.org/autocomplete/search;1?name=urlinline michael@0: michael@0: function urlInlineComplete() michael@0: { michael@0: this._loadPrefs(true); michael@0: Services.obs.addObserver(this, kTopicShutdown, true); michael@0: } michael@0: michael@0: urlInlineComplete.prototype = { michael@0: michael@0: ///////////////////////////////////////////////////////////////////////////////// michael@0: //// Database and query getters michael@0: michael@0: __db: null, michael@0: michael@0: get _db() michael@0: { michael@0: if (!this.__db && this._autofillEnabled) { michael@0: this.__db = PlacesUtils.history.DBConnection.clone(true); michael@0: } michael@0: return this.__db; michael@0: }, michael@0: michael@0: __hostQuery: null, michael@0: michael@0: get _hostQuery() michael@0: { michael@0: if (!this.__hostQuery) { michael@0: // Add a trailing slash at the end of the hostname, since we always michael@0: // want to complete up to and including a URL separator. michael@0: this.__hostQuery = this._db.createAsyncStatement( michael@0: "/* do not warn (bug no): could index on (typed,frecency) but not worth it */ " michael@0: + "SELECT host || '/', prefix || host || '/' " michael@0: + "FROM moz_hosts " michael@0: + "WHERE host BETWEEN :search_string AND :search_string || X'FFFF' " michael@0: + "AND frecency <> 0 " michael@0: + (this._autofillTyped ? "AND typed = 1 " : "") michael@0: + "ORDER BY frecency DESC " michael@0: + "LIMIT 1" michael@0: ); michael@0: } michael@0: return this.__hostQuery; michael@0: }, michael@0: michael@0: __urlQuery: null, michael@0: michael@0: get _urlQuery() michael@0: { michael@0: if (!this.__urlQuery) { michael@0: this.__urlQuery = this._db.createAsyncStatement( michael@0: "/* do not warn (bug no): can't use an index */ " michael@0: + "SELECT h.url " michael@0: + "FROM moz_places h " michael@0: + "WHERE h.frecency <> 0 " michael@0: + (this._autofillTyped ? "AND h.typed = 1 " : "") michael@0: + "AND AUTOCOMPLETE_MATCH(:searchString, h.url, " michael@0: + "h.title, '', " michael@0: + "h.visit_count, h.typed, 0, 0, " michael@0: + ":matchBehavior, :searchBehavior) " michael@0: + "ORDER BY h.frecency DESC, h.id DESC " michael@0: + "LIMIT 1" michael@0: ); michael@0: } michael@0: return this.__urlQuery; michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsIAutoCompleteSearch michael@0: michael@0: startSearch: function UIC_startSearch(aSearchString, aSearchParam, michael@0: aPreviousResult, aListener) michael@0: { michael@0: // Stop the search in case the controller has not taken care of it. michael@0: if (this._pendingQuery) { michael@0: this.stopSearch(); michael@0: } michael@0: michael@0: // We want to store the original string with no leading or trailing michael@0: // whitespace for case sensitive searches. michael@0: this._originalSearchString = aSearchString; michael@0: this._currentSearchString = michael@0: fixupSearchText(this._originalSearchString.toLowerCase()); michael@0: // The protocol and the host are lowercased by nsIURI, so it's fine to michael@0: // lowercase the typed prefix to add it back to the results later. michael@0: this._strippedPrefix = this._originalSearchString.slice( michael@0: 0, this._originalSearchString.length - this._currentSearchString.length michael@0: ).toLowerCase(); michael@0: michael@0: this._result = Cc["@mozilla.org/autocomplete/simple-result;1"]. michael@0: createInstance(Ci.nsIAutoCompleteSimpleResult); michael@0: this._result.setSearchString(aSearchString); michael@0: this._result.setTypeAheadResult(true); michael@0: michael@0: this._listener = aListener; michael@0: michael@0: // Don't autoFill if the search term is recognized as a keyword, otherwise michael@0: // it will override default keywords behavior. Note that keywords are michael@0: // hashed on first use, so while the first query may delay a little bit, michael@0: // next ones will just hit the memory hash. michael@0: if (this._currentSearchString.length == 0 || !this._db || michael@0: PlacesUtils.bookmarks.getURIForKeyword(this._currentSearchString)) { michael@0: this._finishSearch(); michael@0: return; michael@0: } michael@0: michael@0: // Don't try to autofill if the search term includes any whitespace. michael@0: // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH michael@0: // tokenizer ends up trimming the search string and returning a value michael@0: // that doesn't match it, or is even shorter. michael@0: if (/\s/.test(this._currentSearchString)) { michael@0: this._finishSearch(); michael@0: return; michael@0: } michael@0: michael@0: // Hosts have no "/" in them. michael@0: let lastSlashIndex = this._currentSearchString.lastIndexOf("/"); michael@0: michael@0: // Search only URLs if there's a slash in the search string... michael@0: if (lastSlashIndex != -1) { michael@0: // ...but not if it's exactly at the end of the search string. michael@0: if (lastSlashIndex < this._currentSearchString.length - 1) michael@0: this._queryURL(); michael@0: else michael@0: this._finishSearch(); michael@0: return; michael@0: } michael@0: michael@0: // Do a synchronous search on the table of hosts. michael@0: let query = this._hostQuery; michael@0: query.params.search_string = this._currentSearchString.toLowerCase(); michael@0: // This is just to measure the delay to reach the UI, not the query time. michael@0: TelemetryStopwatch.start(DOMAIN_QUERY_TELEMETRY); michael@0: let ac = this; michael@0: let wrapper = new AutoCompleteStatementCallbackWrapper(this, { michael@0: handleResult: function (aResultSet) { michael@0: let row = aResultSet.getNextRow(); michael@0: let trimmedHost = row.getResultByIndex(0); michael@0: let untrimmedHost = row.getResultByIndex(1); michael@0: // If the untrimmed value doesn't preserve the user's input just michael@0: // ignore it and complete to the found host. michael@0: if (untrimmedHost && michael@0: !untrimmedHost.toLowerCase().contains(ac._originalSearchString.toLowerCase())) { michael@0: untrimmedHost = null; michael@0: } michael@0: michael@0: ac._result.appendMatch(ac._strippedPrefix + trimmedHost, "", "", "", untrimmedHost); michael@0: michael@0: // handleCompletion() will cause the result listener to be called, and michael@0: // will display the result in the UI. michael@0: }, michael@0: michael@0: handleError: function (aError) { michael@0: Components.utils.reportError( michael@0: "URL Inline Complete: An async statement encountered an " + michael@0: "error: " + aError.result + ", '" + aError.message + "'"); michael@0: }, michael@0: michael@0: handleCompletion: function (aReason) { michael@0: TelemetryStopwatch.finish(DOMAIN_QUERY_TELEMETRY); michael@0: ac._finishSearch(); michael@0: } michael@0: }, this._db); michael@0: this._pendingQuery = wrapper.executeAsync([query]); michael@0: }, michael@0: michael@0: /** michael@0: * Execute an asynchronous search through places, and complete michael@0: * up to the next URL separator. michael@0: */ michael@0: _queryURL: function UIC__queryURL() michael@0: { michael@0: // The URIs in the database are fixed up, so we can match on a lowercased michael@0: // host, but the path must be matched in a case sensitive way. michael@0: let pathIndex = michael@0: this._originalSearchString.indexOf("/", this._strippedPrefix.length); michael@0: this._currentSearchString = fixupSearchText( michael@0: this._originalSearchString.slice(0, pathIndex).toLowerCase() + michael@0: this._originalSearchString.slice(pathIndex) michael@0: ); michael@0: michael@0: // Within the standard autocomplete query, we only search the beginning michael@0: // of URLs for 1 result. michael@0: let query = this._urlQuery; michael@0: let (params = query.params) { michael@0: params.matchBehavior = MATCH_BEGINNING_CASE_SENSITIVE; michael@0: params.searchBehavior = Ci.mozIPlacesAutoComplete["BEHAVIOR_URL"]; michael@0: params.searchString = this._currentSearchString; michael@0: } michael@0: michael@0: // Execute the query. michael@0: let ac = this; michael@0: let wrapper = new AutoCompleteStatementCallbackWrapper(this, { michael@0: handleResult: function(aResultSet) { michael@0: let row = aResultSet.getNextRow(); michael@0: let value = row.getResultByIndex(0); michael@0: let url = fixupSearchText(value); michael@0: michael@0: let prefix = value.slice(0, value.length - stripPrefix(value).length); michael@0: michael@0: // We must complete the URL up to the next separator (which is /, ? or #). michael@0: let separatorIndex = url.slice(ac._currentSearchString.length) michael@0: .search(/[\/\?\#]/); michael@0: if (separatorIndex != -1) { michael@0: separatorIndex += ac._currentSearchString.length; michael@0: if (url[separatorIndex] == "/") { michael@0: separatorIndex++; // Include the "/" separator michael@0: } michael@0: url = url.slice(0, separatorIndex); michael@0: } michael@0: michael@0: // Add the result. michael@0: // If the untrimmed value doesn't preserve the user's input just michael@0: // ignore it and complete to the found url. michael@0: let untrimmedURL = prefix + url; michael@0: if (untrimmedURL && michael@0: !untrimmedURL.toLowerCase().contains(ac._originalSearchString.toLowerCase())) { michael@0: untrimmedURL = null; michael@0: } michael@0: michael@0: ac._result.appendMatch(ac._strippedPrefix + url, "", "", "", untrimmedURL); michael@0: michael@0: // handleCompletion() will cause the result listener to be called, and michael@0: // will display the result in the UI. michael@0: }, michael@0: michael@0: handleError: function(aError) { michael@0: Components.utils.reportError( michael@0: "URL Inline Complete: An async statement encountered an " + michael@0: "error: " + aError.result + ", '" + aError.message + "'"); michael@0: }, michael@0: michael@0: handleCompletion: function(aReason) { michael@0: ac._finishSearch(); michael@0: } michael@0: }, this._db); michael@0: this._pendingQuery = wrapper.executeAsync([query]); michael@0: }, michael@0: michael@0: stopSearch: function UIC_stopSearch() michael@0: { michael@0: delete this._originalSearchString; michael@0: delete this._currentSearchString; michael@0: delete this._result; michael@0: delete this._listener; michael@0: michael@0: if (this._pendingQuery) { michael@0: this._pendingQuery.cancel(); michael@0: delete this._pendingQuery; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Loads the preferences that we care about. michael@0: * michael@0: * @param [optional] aRegisterObserver michael@0: * Indicates if the preference observer should be added or not. The michael@0: * default value is false. michael@0: */ michael@0: _loadPrefs: function UIC_loadPrefs(aRegisterObserver) michael@0: { michael@0: let prefBranch = Services.prefs.getBranch(kBrowserUrlbarBranch); michael@0: let autocomplete = safePrefGetter(prefBranch, michael@0: kBrowserUrlbarAutocompleteEnabledPref, michael@0: true); michael@0: let autofill = safePrefGetter(prefBranch, michael@0: kBrowserUrlbarAutofillPref, michael@0: true); michael@0: this._autofillEnabled = autocomplete && autofill; michael@0: this._autofillTyped = safePrefGetter(prefBranch, michael@0: kBrowserUrlbarAutofillTypedPref, michael@0: true); michael@0: if (aRegisterObserver) { michael@0: Services.prefs.addObserver(kBrowserUrlbarBranch, this, true); michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsIAutoCompleteSearchDescriptor michael@0: get searchType() Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsIObserver michael@0: michael@0: observe: function UIC_observe(aSubject, aTopic, aData) michael@0: { michael@0: if (aTopic == kTopicShutdown) { michael@0: this._closeDatabase(); michael@0: } michael@0: else if (aTopic == kPrefChanged && michael@0: (aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillPref || michael@0: aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutocompleteEnabledPref || michael@0: aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillTypedPref)) { michael@0: let previousAutofillTyped = this._autofillTyped; michael@0: this._loadPrefs(); michael@0: if (!this._autofillEnabled) { michael@0: this.stopSearch(); michael@0: this._closeDatabase(); michael@0: } michael@0: else if (this._autofillTyped != previousAutofillTyped) { michael@0: // Invalidate the statements to update them for the new typed status. michael@0: this._invalidateStatements(); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Finalizes and invalidates cached statements. michael@0: */ michael@0: _invalidateStatements: function UIC_invalidateStatements() michael@0: { michael@0: // Finalize the statements that we have used. michael@0: let stmts = [ michael@0: "__hostQuery", michael@0: "__urlQuery", michael@0: ]; michael@0: for (let i = 0; i < stmts.length; i++) { michael@0: // We do not want to create any query we haven't already created, so michael@0: // see if it is a getter first. michael@0: if (this[stmts[i]]) { michael@0: this[stmts[i]].finalize(); michael@0: this[stmts[i]] = null; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Closes the database. michael@0: */ michael@0: _closeDatabase: function UIC_closeDatabase() michael@0: { michael@0: this._invalidateStatements(); michael@0: if (this.__db) { michael@0: this._db.asyncClose(); michael@0: this.__db = null; michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// urlInlineComplete michael@0: michael@0: _finishSearch: function UIC_finishSearch() michael@0: { michael@0: // Notify the result object michael@0: let result = this._result; michael@0: michael@0: if (result.matchCount) { michael@0: result.setDefaultIndex(0); michael@0: result.setSearchResult(Ci.nsIAutoCompleteResult["RESULT_SUCCESS"]); michael@0: } else { michael@0: result.setDefaultIndex(-1); michael@0: result.setSearchResult(Ci.nsIAutoCompleteResult["RESULT_NOMATCH"]); michael@0: } michael@0: michael@0: this._listener.onSearchResult(this, result); michael@0: this.stopSearch(); michael@0: }, michael@0: michael@0: isSearchComplete: function UIC_isSearchComplete() michael@0: { michael@0: return this._pendingQuery == null; michael@0: }, michael@0: michael@0: isPendingSearch: function UIC_isPendingSearch(aHandle) michael@0: { michael@0: return this._pendingQuery == aHandle; michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsISupports michael@0: michael@0: classID: Components.ID("c88fae2d-25cf-4338-a1f4-64a320ea7440"), michael@0: michael@0: _xpcom_factory: XPCOMUtils.generateSingletonFactory(urlInlineComplete), michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsIAutoCompleteSearch, michael@0: Ci.nsIAutoCompleteSearchDescriptor, michael@0: Ci.nsIObserver, michael@0: Ci.nsISupportsWeakReference, michael@0: ]) michael@0: }; michael@0: michael@0: let components = [nsPlacesAutoComplete, urlInlineComplete]; michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);