forked from mirrors/gecko-dev
FormHandler is a central place for heuristics that other components like the LoginManager and FormAutofill rely on. This patch moves the heuristics that detect page navigations to the FormHandler. It also applies two changes: - Heuristic to capture on page navigation no longer relies on the process' active element in FormAutofill - Capturing in cross-origin frames is introduced Introduced page navigation heuristic: When LoginManager/FormAutofill detect a form that they expect a submission for, a FormHandler actor pair is created in the current window context, which registers the web progress listener that listens for "page navigations", e.g. location changes of the observed window/document or history session changes. - If the form is in a same-orign frame, we register the listener only at the top level. - If the form is in a cross-origin frame, we additionally set up a listener with the root of the cross-origin process, so that we are aware of page navigations in both processes. When a page navigation is observed, all existing (same-origin and cross-origin) FormHandler parents in the browsing context subtree notify their children. (Note: We don't create any new actors in this step, because they won't have any form to submit anyway). When the corresponding FormHandlerChild(ren) are notified of the page navigation, they fire the "form-submission-detected" event. On "form-submission-detected" event: - The LoginManagerChild instance(s) kept track of all modified login forms and infers capturing them. - The FormAutofillChild instance(s) kept track of all identified formautofill forms and infers capturing them. Differential Revision: https://phabricator.services.mozilla.com/D204927
738 lines
22 KiB
JavaScript
738 lines
22 KiB
JavaScript
/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AddressResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
|
|
AutoCompleteChild: "resource://gre/actors/AutoCompleteChild.sys.mjs",
|
|
AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs",
|
|
CreditCardResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
|
|
GenericAutocompleteItem: "resource://gre/modules/FillHelpers.sys.mjs",
|
|
InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.sys.mjs",
|
|
FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
|
|
FormAutofillContent: "resource://autofill/FormAutofillContent.sys.mjs",
|
|
FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
|
|
FormScenarios: "resource://gre/modules/FormScenarios.sys.mjs",
|
|
FormStateManager: "resource://gre/modules/shared/FormStateManager.sys.mjs",
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
|
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
|
FORM_SUBMISSION_REASON: "resource://gre/actors/FormHandlerChild.sys.mjs",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"DELEGATE_AUTOCOMPLETE",
|
|
"toolkit.autocomplete.delegate",
|
|
false
|
|
);
|
|
|
|
const formFillController = Cc[
|
|
"@mozilla.org/satchel/form-fill-controller;1"
|
|
].getService(Ci.nsIFormFillController);
|
|
|
|
/**
|
|
* Handles content's interactions for the frame.
|
|
*/
|
|
export class FormAutofillChild extends JSWindowActorChild {
|
|
// Flag indicating whether the form is waiting to be filled by Autofill.
|
|
#autofillPending = false;
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.log = lazy.FormAutofill.defineLogGetter(this, "FormAutofillChild");
|
|
this.debug("init");
|
|
|
|
this._nextHandleElement = null;
|
|
this._hasDOMContentLoadedHandler = false;
|
|
this._hasPendingTask = false;
|
|
|
|
/**
|
|
* @type {FormAutofillFieldDetailsManager} handling state management of current forms and handlers.
|
|
*/
|
|
this._fieldDetailsManager = new lazy.FormStateManager(
|
|
this.formSubmitted.bind(this),
|
|
this.formAutofilled.bind(this)
|
|
);
|
|
|
|
lazy.AutoCompleteChild.addPopupStateListener(this);
|
|
|
|
/**
|
|
* Tracks whether the last form submission was triggered by a form submit event,
|
|
* if so we'll ignore the page navigation that follows
|
|
*/
|
|
this.isFollowingSubmitEvent = false;
|
|
}
|
|
|
|
didDestroy() {
|
|
this._fieldDetailsManager.didDestroy();
|
|
|
|
lazy.AutoCompleteChild.removePopupStateListener(this);
|
|
}
|
|
|
|
popupStateChanged(messageName, _data, _target) {
|
|
if (!lazy.FormAutofill.isAutofillEnabled) {
|
|
return;
|
|
}
|
|
|
|
switch (messageName) {
|
|
case "AutoComplete:PopupClosed": {
|
|
this.onPopupClosed();
|
|
break;
|
|
}
|
|
case "AutoComplete:PopupOpened": {
|
|
this.onPopupOpened();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Identifies and marks each autofill field
|
|
*/
|
|
identifyAutofillFields() {
|
|
if (this._hasPendingTask) {
|
|
return;
|
|
}
|
|
this._hasPendingTask = true;
|
|
|
|
lazy.setTimeout(() => {
|
|
const element = this._nextHandleElement;
|
|
this.debug(
|
|
`identifyAutofillFields: ${element.ownerDocument.location?.hostname}`
|
|
);
|
|
|
|
if (
|
|
lazy.DELEGATE_AUTOCOMPLETE ||
|
|
!lazy.FormAutofillContent.savedFieldNames
|
|
) {
|
|
this.debug("identifyAutofillFields: savedFieldNames are not known yet");
|
|
|
|
// Init can be asynchronous because we don't need anything from the parent
|
|
// at this point.
|
|
this.sendAsyncMessage("FormAutofill:InitStorage");
|
|
}
|
|
|
|
const validDetails =
|
|
this._fieldDetailsManager.identifyAutofillFields(element);
|
|
|
|
validDetails?.forEach(detail =>
|
|
this._markAsAutofillField(detail.element)
|
|
);
|
|
if (validDetails.length) {
|
|
if (lazy.FormAutofill.captureOnFormRemoval) {
|
|
this.registerDOMDocFetchSuccessEventListener();
|
|
}
|
|
|
|
this.manager.getActor("FormHandler").registerFormSubmissionInterest();
|
|
}
|
|
|
|
this._hasPendingTask = false;
|
|
this._nextHandleElement = null;
|
|
// This is for testing purpose only which sends a notification to indicate that the
|
|
// form has been identified, and ready to open popup.
|
|
this.sendAsyncMessage("FormAutofill:FieldsIdentified");
|
|
this.updateActiveInput();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* We received a form-submission-detected event because
|
|
* the page was navigated.
|
|
*/
|
|
onPageNavigation() {
|
|
if (!lazy.FormAutofill.captureOnPageNavigation) {
|
|
return;
|
|
}
|
|
|
|
if (this.isFollowingSubmitEvent) {
|
|
// The next page navigation should be handled as form submission again
|
|
this.isFollowingSubmitEvent = false;
|
|
return;
|
|
}
|
|
let weakIdentifiedForms = ChromeUtils.nondeterministicGetWeakMapKeys(
|
|
this._fieldDetailsManager._formsDetails
|
|
);
|
|
const formSubmissionReason = lazy.FORM_SUBMISSION_REASON.PAGE_NAVIGATION;
|
|
|
|
for (const form of weakIdentifiedForms) {
|
|
// Disconnected forms are captured by the form removal heuristic
|
|
if (!form.isConnected) {
|
|
continue;
|
|
}
|
|
this.formSubmitted(form, formSubmissionReason);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* After a focusin event and after we identify formautofill fields,
|
|
* we set up an event listener for the DOMDocFetchSuccess event
|
|
*/
|
|
registerDOMDocFetchSuccessEventListener() {
|
|
this.document.setNotifyFetchSuccess(true);
|
|
|
|
// Is removed after a DOMDocFetchSuccess event (bug 1864855)
|
|
/* eslint-disable mozilla/balanced-listeners */
|
|
this.docShell.chromeEventHandler.addEventListener(
|
|
"DOMDocFetchSuccess",
|
|
this,
|
|
true
|
|
);
|
|
}
|
|
|
|
/**
|
|
* After a DOMDocFetchSuccess event, we register an event listener for the DOMFormRemoved event
|
|
*/
|
|
registerDOMFormRemovedEventListener() {
|
|
this.document.setNotifyFormOrPasswordRemoved(true);
|
|
|
|
// Is removed after a DOMFormRemoved event (bug 1864855)
|
|
/* eslint-disable mozilla/balanced-listeners */
|
|
this.docShell.chromeEventHandler.addEventListener(
|
|
"DOMFormRemoved",
|
|
this,
|
|
true
|
|
);
|
|
}
|
|
|
|
/**
|
|
* After a DOMDocFetchSuccess event we remove the DOMDocFetchSuccess event listener
|
|
*/
|
|
unregisterDOMDocFetchSuccessEventListener() {
|
|
this.document.setNotifyFetchSuccess(false);
|
|
this.docShell.chromeEventHandler.removeEventListener(
|
|
"DOMDocFetchSuccess",
|
|
this
|
|
);
|
|
}
|
|
|
|
/**
|
|
* After a DOMFormRemoved event we remove the DOMFormRemoved event listener
|
|
*/
|
|
unregisterDOMFormRemovedEventListener() {
|
|
this.document.setNotifyFormOrPasswordRemoved(false);
|
|
this.docShell.chromeEventHandler.removeEventListener(
|
|
"DOMFormRemoved",
|
|
this
|
|
);
|
|
}
|
|
|
|
shouldIgnoreFormAutofillEvent(event) {
|
|
let nodePrincipal = event.target.nodePrincipal;
|
|
return nodePrincipal.isSystemPrincipal || nodePrincipal.schemeIs("about");
|
|
}
|
|
|
|
handleEvent(evt) {
|
|
if (!evt.isTrusted) {
|
|
return;
|
|
}
|
|
if (this.shouldIgnoreFormAutofillEvent(evt)) {
|
|
return;
|
|
}
|
|
|
|
if (!this.windowContext) {
|
|
// !this.windowContext must not be null, because we need the
|
|
// windowContext and/or docShell to (un)register form submission listeners
|
|
return;
|
|
}
|
|
|
|
switch (evt.type) {
|
|
case "focusin": {
|
|
if (lazy.FormAutofill.isAutofillEnabled) {
|
|
this.onFocusIn(evt);
|
|
}
|
|
break;
|
|
}
|
|
case "DOMFormRemoved": {
|
|
this.onDOMFormRemoved(evt);
|
|
break;
|
|
}
|
|
case "DOMDocFetchSuccess": {
|
|
this.onDOMDocFetchSuccess();
|
|
break;
|
|
}
|
|
case "form-submission-detected": {
|
|
if (lazy.FormAutofill.isAutofillEnabled) {
|
|
const formElement = evt.detail.form;
|
|
const formSubmissionReason = evt.detail.reason;
|
|
this.onFormSubmission(formElement, formSubmissionReason);
|
|
}
|
|
break;
|
|
}
|
|
|
|
default: {
|
|
throw new Error("Unexpected event type");
|
|
}
|
|
}
|
|
}
|
|
|
|
onFocusIn(evt) {
|
|
this.updateActiveInput();
|
|
|
|
const element = evt.target;
|
|
if (!lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element)) {
|
|
return;
|
|
}
|
|
|
|
this._nextHandleElement = element;
|
|
const doc = element.ownerDocument;
|
|
if (doc.readyState === "loading") {
|
|
// For auto-focused input, we might receive focus event before document becomes ready.
|
|
// When this happens, run field identification after receiving `DOMContentLoaded` event
|
|
if (!this._hasDOMContentLoadedHandler) {
|
|
this._hasDOMContentLoadedHandler = true;
|
|
doc.addEventListener(
|
|
"DOMContentLoaded",
|
|
() => this.identifyAutofillFields(),
|
|
{ once: true }
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.identifyAutofillFields();
|
|
}
|
|
|
|
/**
|
|
* Handle form-submission-detected event (dispatched by FormHandlerChild)
|
|
*
|
|
* Depending on the heuristic that detected the form submission,
|
|
* the form that is submitted is retrieved differently
|
|
*
|
|
* @param {HTMLFormElement} form that is being submitted
|
|
* @param {string} reason heuristic that detected the form submission
|
|
* (see FormHandlerChild.FORM_SUBMISSION_REASON)
|
|
*/
|
|
onFormSubmission(form, reason) {
|
|
switch (reason) {
|
|
case lazy.FORM_SUBMISSION_REASON.PAGE_NAVIGATION:
|
|
this.onPageNavigation();
|
|
break;
|
|
case lazy.FORM_SUBMISSION_REASON.FORM_SUBMIT_EVENT:
|
|
this.formSubmitted(form, reason);
|
|
break;
|
|
default:
|
|
throw new Error("Unknown submission reason");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle the DOMFormRemoved event.
|
|
*
|
|
* Infers a form submission when the form is removed
|
|
* after a successful fetch or XHR request.
|
|
*
|
|
* @param {Event} evt DOMFormRemoved
|
|
*/
|
|
onDOMFormRemoved(evt) {
|
|
const formSubmissionReason =
|
|
lazy.FORM_SUBMISSION_REASON.FORM_REMOVAL_AFTER_FETCH;
|
|
|
|
this.formSubmitted(evt.target, formSubmissionReason);
|
|
}
|
|
|
|
/**
|
|
* Handle the DOMDocFetchSuccess event.
|
|
*
|
|
* Sets up an event listener for the DOMFormRemoved event
|
|
* and unregisters the event listener for DOMDocFetchSuccess event.
|
|
*/
|
|
onDOMDocFetchSuccess() {
|
|
this.registerDOMFormRemovedEventListener();
|
|
|
|
this.unregisterDOMDocFetchSuccessEventListener();
|
|
}
|
|
|
|
async receiveMessage(message) {
|
|
if (!lazy.FormAutofill.isAutofillEnabled) {
|
|
return;
|
|
}
|
|
|
|
switch (message.name) {
|
|
case "FormAutofill:PreviewProfile": {
|
|
this.previewProfile(message.data);
|
|
break;
|
|
}
|
|
case "FormAutofill:ClearForm": {
|
|
this.clearForm();
|
|
break;
|
|
}
|
|
case "FormAutofill:FillForm": {
|
|
await this.autofillFields(message.data);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
get activeFieldDetail() {
|
|
return this._fieldDetailsManager.activeFieldDetail;
|
|
}
|
|
|
|
get activeFormDetails() {
|
|
return this._fieldDetailsManager.activeFormDetails;
|
|
}
|
|
|
|
get activeInput() {
|
|
return this._fieldDetailsManager.activeInput;
|
|
}
|
|
|
|
get activeHandler() {
|
|
return this._fieldDetailsManager.activeHandler;
|
|
}
|
|
|
|
get activeSection() {
|
|
return this._fieldDetailsManager.activeSection;
|
|
}
|
|
|
|
/**
|
|
* Handle a form submission and early return when:
|
|
* 1. In private browsing mode.
|
|
* 2. Could not map any autofill handler by form element.
|
|
* 3. Number of filled fields is less than autofill threshold
|
|
*
|
|
* @param {HTMLElement} formElement Root element which receives submit event.
|
|
* @param {string} formSubmissionReason Reason for invoking the form submission
|
|
* (see options for FORM_SUBMISSION_REASON in FormAutofillUtils))
|
|
* @param {Window} domWin Content window; passed for unit tests and when
|
|
* invoked by the FormAutofillSection
|
|
* @param {object} handler FormAutofillHander, if known by caller
|
|
*/
|
|
formSubmitted(
|
|
formElement,
|
|
formSubmissionReason,
|
|
domWin = formElement.ownerGlobal,
|
|
handler = undefined
|
|
) {
|
|
this.debug(`Handling form submission - infered by ${formSubmissionReason}`);
|
|
|
|
lazy.AutofillTelemetry.recordFormSubmissionHeuristicCount(
|
|
formSubmissionReason
|
|
);
|
|
|
|
if (!lazy.FormAutofill.isAutofillEnabled) {
|
|
this.debug("Form Autofill is disabled");
|
|
return;
|
|
}
|
|
|
|
// The `domWin` truthiness test is used by unit tests to bypass this check.
|
|
if (domWin && lazy.PrivateBrowsingUtils.isContentWindowPrivate(domWin)) {
|
|
this.debug("Ignoring submission in a private window");
|
|
return;
|
|
}
|
|
|
|
handler = handler || this._fieldDetailsManager._getFormHandler(formElement);
|
|
const records = this._fieldDetailsManager.getRecords(formElement, handler);
|
|
|
|
if (!records || !handler) {
|
|
this.debug("Form element could not map to an existing handler");
|
|
return;
|
|
}
|
|
|
|
// Unregister the form submission listeners after handling a form submission
|
|
this.debug("Unregistering form submission listeners");
|
|
this.unregisterDOMDocFetchSuccessEventListener();
|
|
this.unregisterDOMFormRemovedEventListener();
|
|
|
|
// After a form submit event follows (most likely) a page navigation, so we set this flag
|
|
// to not handle the following one as form submission in order to avoid re-submitting the same form.
|
|
// Ideally, we should keep a record of the last submitted form details and based on that we
|
|
// should decide if we want to submit a form (bug 1895437)
|
|
this.isFollowingSubmitEvent = true;
|
|
|
|
[records.address, records.creditCard].forEach((rs, idx) => {
|
|
lazy.AutofillTelemetry.recordSubmittedSectionCount(
|
|
idx == 0
|
|
? lazy.AutofillTelemetry.ADDRESS
|
|
: lazy.AutofillTelemetry.CREDIT_CARD,
|
|
rs?.length
|
|
);
|
|
|
|
rs?.forEach(r => {
|
|
lazy.AutofillTelemetry.recordFormInteractionEvent(
|
|
"submitted",
|
|
r.section,
|
|
{
|
|
record: r,
|
|
form: handler.form,
|
|
}
|
|
);
|
|
delete r.section;
|
|
});
|
|
});
|
|
|
|
this.sendAsyncMessage("FormAutofill:OnFormSubmit", records);
|
|
}
|
|
|
|
formAutofilled() {
|
|
lazy.FormAutofillContent.showPopup();
|
|
}
|
|
|
|
/**
|
|
* All active items should be updated according the active element of
|
|
* `formFillController.focusedInput`. All of them including element,
|
|
* handler, section, and field detail, can be retrieved by their own getters.
|
|
*
|
|
* @param {HTMLElement|null} element The active item should be updated based
|
|
* on this or `formFillController.focusedInput` will be taken.
|
|
*/
|
|
updateActiveInput(element) {
|
|
element = element || formFillController.focusedInput;
|
|
if (!element) {
|
|
this.debug("updateActiveElement: no element selected");
|
|
return;
|
|
}
|
|
lazy.FormAutofillContent.updateActiveAutofillChild(this);
|
|
|
|
this._fieldDetailsManager.updateActiveInput(element);
|
|
this.debug("updateActiveElement: checking for popup-on-focus");
|
|
// We know this element just received focus. If it's a credit card field,
|
|
// open its popup.
|
|
if (this.#autofillPending) {
|
|
this.debug("updateActiveElement: skipping check; autofill is imminent");
|
|
} else if (element.value?.length !== 0) {
|
|
this.debug(
|
|
`updateActiveElement: Not opening popup because field is not empty.`
|
|
);
|
|
} else {
|
|
this.debug(
|
|
"updateActiveElement: checking if empty field is cc-*: ",
|
|
this.activeFieldDetail?.fieldName
|
|
);
|
|
|
|
if (
|
|
this.activeFieldDetail?.fieldName?.startsWith("cc-") ||
|
|
AppConstants.platform === "android"
|
|
) {
|
|
lazy.FormAutofillContent.showPopup();
|
|
}
|
|
}
|
|
}
|
|
|
|
clearForm() {
|
|
if (!this.activeSection) {
|
|
return;
|
|
}
|
|
|
|
this.activeSection.clearPopulatedForm();
|
|
|
|
let fieldName = this.activeFieldDetail?.fieldName;
|
|
if (lazy.FormAutofillUtils.isCreditCardField(fieldName)) {
|
|
lazy.AutofillTelemetry.recordFormInteractionEvent(
|
|
"cleared",
|
|
this.activeSection,
|
|
{ fieldName }
|
|
);
|
|
}
|
|
}
|
|
|
|
get lastProfileAutoCompleteResult() {
|
|
return this.manager.getActor("AutoComplete")?.lastProfileAutoCompleteResult;
|
|
}
|
|
|
|
get lastProfileAutoCompleteFocusedInput() {
|
|
return this.manager.getActor("AutoComplete")
|
|
?.lastProfileAutoCompleteFocusedInput;
|
|
}
|
|
|
|
previewProfile(profile) {
|
|
if (profile && this.activeSection) {
|
|
const adaptedProfile = this.activeSection.getAdaptedProfiles([
|
|
profile,
|
|
])[0];
|
|
this.activeSection.previewFormFields(adaptedProfile);
|
|
} else {
|
|
this.activeSection.clearPreviewedFormFields();
|
|
}
|
|
}
|
|
|
|
async autofillFields(profile) {
|
|
this.#autofillPending = true;
|
|
Services.obs.notifyObservers(null, "autofill-fill-starting");
|
|
try {
|
|
Services.obs.notifyObservers(null, "autofill-fill-starting");
|
|
await this.activeHandler.autofillFormFields(profile);
|
|
Services.obs.notifyObservers(null, "autofill-fill-complete");
|
|
} finally {
|
|
this.#autofillPending = false;
|
|
}
|
|
}
|
|
|
|
onPopupClosed() {
|
|
this.debug("Popup has closed.");
|
|
this.activeSection?.clearPreviewedFormFields();
|
|
}
|
|
|
|
onPopupOpened() {
|
|
this.debug(
|
|
"Popup has opened, automatic =",
|
|
formFillController.passwordPopupAutomaticallyOpened
|
|
);
|
|
|
|
let fieldName = this.activeFieldDetail?.fieldName;
|
|
if (fieldName && this.activeSection) {
|
|
lazy.AutofillTelemetry.recordFormInteractionEvent(
|
|
"popup_shown",
|
|
this.activeSection,
|
|
{ fieldName }
|
|
);
|
|
}
|
|
}
|
|
|
|
_markAsAutofillField(field) {
|
|
// Since Form Autofill popup is only for input element, any non-Input
|
|
// element should be excluded here.
|
|
if (!HTMLInputElement.isInstance(field)) {
|
|
return;
|
|
}
|
|
|
|
this.manager
|
|
.getActor("AutoComplete")
|
|
?.markAsAutoCompletableField(field, this);
|
|
}
|
|
|
|
get actorName() {
|
|
return "FormAutofill";
|
|
}
|
|
|
|
/**
|
|
* Get the search options when searching for autocomplete entries in the parent
|
|
*
|
|
* @param {HTMLInputElement} input - The input element to search for autocompelte entries
|
|
* @returns {object} the search options for the input
|
|
*/
|
|
getAutoCompleteSearchOption(input) {
|
|
const fieldDetail = this._fieldDetailsManager
|
|
._getFormHandler(input)
|
|
?.getFieldDetailByElement(input);
|
|
|
|
const scenarioName = lazy.FormScenarios.detect({ input }).signUpForm
|
|
? "SignUpFormScenario"
|
|
: "";
|
|
return { fieldName: fieldDetail?.fieldName, scenarioName };
|
|
}
|
|
|
|
/**
|
|
* Ask the provider whether it might have autocomplete entry to show
|
|
* for the given input.
|
|
*
|
|
* @param {HTMLInputElement} input - The input element to search for autocompelte entries
|
|
* @returns {boolean} true if we shold search for autocomplete entries
|
|
*/
|
|
shouldSearchForAutoComplete(input) {
|
|
const fieldDetail = this._fieldDetailsManager
|
|
._getFormHandler(input)
|
|
?.getFieldDetailByElement(input);
|
|
if (!fieldDetail) {
|
|
return false;
|
|
}
|
|
const fieldName = fieldDetail.fieldName;
|
|
const isAddressField = lazy.FormAutofillUtils.isAddressField(fieldName);
|
|
const searchPermitted = isAddressField
|
|
? lazy.FormAutofill.isAutofillAddressesEnabled
|
|
: lazy.FormAutofill.isAutofillCreditCardsEnabled;
|
|
// If the specified autofill feature is pref off, do not search
|
|
if (!searchPermitted) {
|
|
return false;
|
|
}
|
|
|
|
// No profile can fill the currently-focused input.
|
|
if (!lazy.FormAutofillContent.savedFieldNames.has(fieldName)) {
|
|
return false;
|
|
}
|
|
|
|
// The current form has already been populated and the field is not
|
|
// an empty credit card field.
|
|
const isCreditCardField =
|
|
lazy.FormAutofillUtils.isCreditCardField(fieldName);
|
|
const isInputAutofilled =
|
|
this.activeHandler.getFilledStateByElement(input) ==
|
|
lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED;
|
|
const filledRecordGUID = this.activeSection.filledRecordGUID;
|
|
if (
|
|
!isInputAutofilled &&
|
|
filledRecordGUID &&
|
|
!(isCreditCardField && this.activeInput.value === "")
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// (address only) less than 3 inputs are covered by all saved fields in the storage.
|
|
if (
|
|
isAddressField &&
|
|
this.activeSection.allFieldNames.filter(field =>
|
|
lazy.FormAutofillContent.savedFieldNames.has(field)
|
|
).length < lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Convert the search result to autocomplete results
|
|
*
|
|
* @param {string} searchString - The string to search for
|
|
* @param {HTMLInputElement} input - The input element to search for autocompelte entries
|
|
* @param {Array<object>} records - autocomplete records
|
|
* @returns {AutocompleteResult}
|
|
*/
|
|
searchResultToAutoCompleteResult(searchString, input, records) {
|
|
if (!records) {
|
|
return null;
|
|
}
|
|
|
|
const entries = records.records;
|
|
const externalEntries = records.externalEntries;
|
|
|
|
const fieldDetail = this._fieldDetailsManager
|
|
._getFormHandler(input)
|
|
?.getFieldDetailByElement(input);
|
|
if (!fieldDetail) {
|
|
return null;
|
|
}
|
|
|
|
const adaptedRecords = this.activeSection.getAdaptedProfiles(entries);
|
|
const isSecure = lazy.InsecurePasswordUtils.isFormSecure(
|
|
this.activeHandler.form
|
|
);
|
|
const isInputAutofilled =
|
|
this.activeHandler.getFilledStateByElement(input) ==
|
|
lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED;
|
|
const allFieldNames = this.activeSection.allFieldNames;
|
|
|
|
const AutocompleteResult = lazy.FormAutofillUtils.isAddressField(
|
|
fieldDetail.fieldName
|
|
)
|
|
? lazy.AddressResult
|
|
: lazy.CreditCardResult;
|
|
|
|
const acResult = new AutocompleteResult(
|
|
searchString,
|
|
fieldDetail.fieldName,
|
|
allFieldNames,
|
|
adaptedRecords,
|
|
{ isSecure, isInputAutofilled }
|
|
);
|
|
|
|
acResult.externalEntries.push(
|
|
...externalEntries.map(
|
|
entry =>
|
|
new lazy.GenericAutocompleteItem(
|
|
entry.image,
|
|
entry.title,
|
|
entry.subtitle,
|
|
entry.fillMessageName,
|
|
entry.fillMessageData
|
|
)
|
|
)
|
|
);
|
|
|
|
return acResult;
|
|
}
|
|
}
|