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