Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
FirefoxRelay: "resource://gre/modules/FirefoxRelay.sys.mjs",
FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "SmartFormFillAutocomplete", () => {
if (AppConstants.MOZ_BUILD_APP != "browser") {
return undefined;
}
try {
return ChromeUtils.importESModule(
// eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
"moz-src:///browser/components/aiwindow/ui/modules/SmartFormFillAutocomplete.sys.mjs"
).SmartFormFillAutocomplete;
} catch (error) {
console.error(`Unable to load SmartFormFillAutocomplete.sys.mjs: ${error}`);
}
return undefined;
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"PREFERENCE_PREFIX_WEIGHT",
"browser.formfill.prefixWeight"
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"PREFERENCE_BOUNDARY_WEIGHT",
"browser.formfill.boundaryWeight"
);
export class FormHistoryParent extends JSWindowActorParent {
receiveMessage({ name, data }) {
switch (name) {
case "FormHistory:FormSubmitEntries":
this.#onFormSubmitEntries(data);
break;
case "FormHistory:AutoCompleteSearchAsync":
return this.#onAutoCompleteSearch(data);
case "FormHistory:RemoveEntry":
this.#onRemoveEntry(data);
break;
}
return undefined;
}
#onFormSubmitEntries(entries) {
// Don't store form history in private browsing sessions, or when the
// browsing context cannot be determined.
if (this.browsingContext?.usePrivateBrowsing ?? true) {
return;
}
const changes = entries
.filter(entry => lazy.FormHistory.isAllowedEntry(entry.name, entry.value))
.map(entry => ({
op: "bump",
fieldname: entry.name,
value: entry.value,
}));
if (changes.length) {
lazy.FormHistory.update(changes);
}
}
get formOrigin() {
return lazy.LoginHelper.getLoginOrigin(
this.manager.documentPrincipal?.originNoSuffix
);
}
async #onAutoCompleteSearch({
searchString,
params,
inputType,
scenarioName,
}) {
searchString = searchString.trim().toLowerCase();
// The search bar manages its own history and is not served here.
if (!lazy.FormHistory.isAllowedFieldname(params?.fieldname)) {
return { formHistoryEntries: [], externalEntries: [] };
}
let formHistoryPromise;
if (
FormHistoryParent.canSearchIncrementally(
searchString,
this.previousSearchString
)
) {
formHistoryPromise = Promise.resolve(
FormHistoryParent.incrementalSearch(
searchString,
this.previousSearchString,
this.previousSearchResult
)
);
} else {
formHistoryPromise = lazy.FormHistory.getAutoCompleteResults(
searchString,
params
);
}
const relayPromise = lazy.FirefoxRelay.autocompleteItemsAsync({
origin: this.formOrigin,
scenarioName,
hasInput: !!searchString.length,
});
const smartFormFillPromise =
lazy.SmartFormFillAutocomplete?.autocompleteItemsAsync({
browsingContext: this.browsingContext,
searchString,
inputType,
}) ?? [];
const [formHistoryEntries, relayEntries, smartFormFillEntries] =
await Promise.all([
formHistoryPromise,
relayPromise,
smartFormFillPromise,
]);
const externalEntries = [...relayEntries, ...smartFormFillEntries];
this.previousSearchString = searchString;
this.previousSearchResult = formHistoryEntries;
return { formHistoryEntries, externalEntries };
}
#onRemoveEntry({ inputName, value, guid }) {
// Removals must be scoped to a specific, allowed fieldname so the
// fieldname always constrains the query.
if (!inputName || !lazy.FormHistory.isAllowedFieldname(inputName)) {
return Promise.resolve();
}
this.previousSearchString = null;
this.previousSearchResult = null;
return lazy.FormHistory.update({
op: "remove",
fieldname: inputName,
value,
guid,
});
}
async onAutoCompleteEntrySelected(message, data) {
switch (message) {
case "FormHistory:RemoveEntry": {
await this.#onRemoveEntry(data);
this.#repopulateAutocompletePopup();
break;
}
}
}
#repopulateAutocompletePopup() {
if (!this.manager || this.manager.isClosed) {
return;
}
this.sendAsyncMessage("FormHistory:RepopulateAutocompletePopup");
}
async searchAutoCompleteEntries(searchString, data) {
const { inputName, inputType, scenarioName } = data;
const params = {
fieldname: inputName,
};
return this.#onAutoCompleteSearch({
searchString,
params,
inputType,
scenarioName,
});
}
static canSearchIncrementally(searchString, previousSearchString) {
previousSearchString ||= "";
return (
previousSearchString.length > 1 &&
searchString.includes(previousSearchString)
);
}
static incrementalSearch(
searchString,
previousSearchString,
previousSearchResult
) {
const searchTokens = searchString.split(/\s+/);
// We have a list of results for a shorter search string, so just
// filter them further based on the new search string and add to a new array.
let filteredEntries = [];
for (const entry of previousSearchResult) {
// Remove results that do not contain the token
// XXX bug 394604 -- .toLowerCase can be wrong for some intl chars
if (searchTokens.some(tok => !entry.textLowerCase.includes(tok))) {
continue;
}
FormHistoryParent.calculateScore(entry, searchString, searchTokens);
filteredEntries.push(entry);
}
filteredEntries.sort((a, b) => b.totalScore - a.totalScore);
return filteredEntries;
}
/*
* calculateScore
*
* entry -- an nsIAutoCompleteResult entry
* searchString -- current value of the input (lowercase)
* searchTokens -- array of tokens of the search string
*
* Returns: an int
*/
static calculateScore(entry, searchString, searchTokens) {
let boundaryCalc = 0;
// for each word, calculate word boundary weights
for (const token of searchTokens) {
if (entry.textLowerCase.startsWith(token)) {
boundaryCalc++;
}
if (entry.textLowerCase.includes(" " + token)) {
boundaryCalc++;
}
}
boundaryCalc = boundaryCalc * lazy.PREFERENCE_BOUNDARY_WEIGHT;
// now add more weight if we have a traditional prefix match and
// multiply boundary bonuses by boundary weight
if (entry.textLowerCase.startsWith(searchString)) {
boundaryCalc += lazy.PREFERENCE_PREFIX_WEIGHT;
}
entry.totalScore = Math.round(entry.frecency * Math.max(1, boundaryCalc));
}
previewFields(_result) {
// Not implemented
}
autofillFields(_result) {
// Not implemented
}
}