tenfourfox/toolkit/components/places/nsPlacesAutoComplete.js
2017-08-25 19:01:13 -07:00

1795 lines
61 KiB
JavaScript

/* -*- 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, "TelemetryStopwatch",
"resource://gre/modules/TelemetryStopwatch.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 typo 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.
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";
}
/* 10.4Fx issue 21. This makes me absolutely nauseous just writing this
because this is about the most naked, improper wallpaper job I've ever
written. But it works. In short, NEVER return RESULT_NOMATCH -- if we
are about to do that, forge a single stub match to make a blank box and
_SUCCESS, and this solves the problem. I am going to take a shower and
try to scrub the disgust from my heaving, vomiting body. */
if (resultCode == "RESULT_NOMATCH") { // well, shoot. let's fudge.
this._result.appendMatch(
"", // blank URL
"", // blank title
"", // no favicon
"favicon" // but use a favicon style -- thus a blank drop-down
);
resultCode = "RESULT_SUCCESS";
} // end issue
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();
// This is just to measure the delay to reach the UI, not the query time.
//TelemetryStopwatch.start(DOMAIN_QUERY_TELEMETRY);
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;
//TelemetryStopwatch.finish(DOMAIN_QUERY_TELEMETRY);
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);