зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1829670 - Decouple autofilled state and field information identified by heuristics r=credential-management-reviewers,sgalich
- Add a "filledStateByElement" to record filled status instread of saving it in `FieldDetails` - Store filled state in FormAutofillHandler instead of FormAutofillSection. - Change `transform` variable stored in fieldDetails to `part` to decouple autofilling related logic from FieldDetails Differential Revision: https://phabricator.services.mozilla.com/D176324
This commit is contained in:
Родитель
5c447033ec
Коммит
163a42f29d
|
@ -203,6 +203,9 @@ function verifySectionFieldDetails(sections, expectedResults) {
|
|||
|
||||
sectionInfo.forEach((field, fieldIndex) => {
|
||||
let expectedField = expectedSectionInfo[fieldIndex];
|
||||
if (!("part" in expectedField)) {
|
||||
expectedField.part = null;
|
||||
}
|
||||
delete field.reason;
|
||||
delete field.elementWeakRef;
|
||||
delete field.confidence;
|
||||
|
|
|
@ -20,28 +20,28 @@ runHeuristicsTest(
|
|||
addressType: "",
|
||||
contactType: "",
|
||||
fieldName: "cc-number",
|
||||
transform: "fullCCNumber => fullCCNumber.slice(0, 4)",
|
||||
part: 1,
|
||||
},
|
||||
{
|
||||
section: "",
|
||||
addressType: "",
|
||||
contactType: "",
|
||||
fieldName: "cc-number",
|
||||
transform: "fullCCNumber => fullCCNumber.slice(4, 8)",
|
||||
part: 2,
|
||||
},
|
||||
{
|
||||
section: "",
|
||||
addressType: "",
|
||||
contactType: "",
|
||||
fieldName: "cc-number",
|
||||
transform: "fullCCNumber => fullCCNumber.slice(8, 12)",
|
||||
part: 3,
|
||||
},
|
||||
{
|
||||
section: "",
|
||||
addressType: "",
|
||||
contactType: "",
|
||||
fieldName: "cc-number",
|
||||
transform: "fullCCNumber => fullCCNumber.slice(12, 16)",
|
||||
part: 4,
|
||||
},
|
||||
{
|
||||
section: "",
|
||||
|
|
|
@ -185,7 +185,9 @@ function run_tests(testcases) {
|
|||
await handler.activeSection.previewFormFields(adaptedProfile);
|
||||
|
||||
for (let field of handler.fieldDetails) {
|
||||
let actual = field.state;
|
||||
let actual = handler.getFilledStateByElement(
|
||||
field.elementWeakRef.get()
|
||||
);
|
||||
let expected = testcase.expectedResultState[field.fieldName];
|
||||
info(`Checking ${field.fieldName} state`);
|
||||
Assert.equal(
|
||||
|
|
|
@ -69,7 +69,8 @@ class AutofillTelemetryBase {
|
|||
let element = fieldDetail.elementWeakRef.get();
|
||||
let state = profile[fieldDetail.fieldName] ? "filled" : "not_filled";
|
||||
if (
|
||||
fieldDetail.state == FIELD_STATES.NORMAL &&
|
||||
section.handler.getFilledStateByElement(element) ==
|
||||
FIELD_STATES.NORMAL &&
|
||||
(HTMLSelectElement.isInstance(element) ||
|
||||
(HTMLInputElement.isInstance(element) && element.value.length))
|
||||
) {
|
||||
|
@ -366,7 +367,8 @@ class CreditCardTelemetry extends AutofillTelemetryBase {
|
|||
let element = fieldDetail.elementWeakRef.get();
|
||||
let state = profile[fieldDetail.fieldName] ? "filled" : "not_filled";
|
||||
if (
|
||||
fieldDetail.state == FIELD_STATES.NORMAL &&
|
||||
section.handler.getFilledStateByElement(element) ==
|
||||
FIELD_STATES.NORMAL &&
|
||||
(HTMLSelectElement.isInstance(element) ||
|
||||
(HTMLInputElement.isInstance(element) && element.value.length))
|
||||
) {
|
||||
|
|
|
@ -146,6 +146,7 @@ AutofillProfileAutoCompleteSearch.prototype = {
|
|||
activeInput,
|
||||
activeSection,
|
||||
activeFieldDetail,
|
||||
activeHandler,
|
||||
savedFieldNames,
|
||||
} = FormAutofillContent;
|
||||
this.forceStop = false;
|
||||
|
@ -157,7 +158,8 @@ AutofillProfileAutoCompleteSearch.prototype = {
|
|||
activeFieldDetail.fieldName
|
||||
);
|
||||
let isInputAutofilled =
|
||||
activeFieldDetail.state == lazy.FIELD_STATES.AUTO_FILLED;
|
||||
activeHandler.getFilledStateByElement(activeInput) ==
|
||||
lazy.FIELD_STATES.AUTO_FILLED;
|
||||
let allFieldNames = activeSection.allFieldNames;
|
||||
let filledRecordGUID = activeSection.filledRecordGUID;
|
||||
|
||||
|
|
|
@ -46,22 +46,12 @@ XPCOMUtils.defineLazyGetter(lazy, "log", () =>
|
|||
const { FIELD_STATES } = FormAutofillUtils;
|
||||
|
||||
class FormAutofillSection {
|
||||
constructor(fieldDetails, winUtils) {
|
||||
this.fieldDetails = fieldDetails;
|
||||
this.filledRecordGUID = null;
|
||||
this.winUtils = winUtils;
|
||||
#focusedInput = null;
|
||||
|
||||
/**
|
||||
* Enum for form autofill MANUALLY_MANAGED_STATES values
|
||||
*/
|
||||
this._FIELD_STATE_ENUM = {
|
||||
// not themed
|
||||
[FIELD_STATES.NORMAL]: null,
|
||||
// highlighted
|
||||
[FIELD_STATES.AUTO_FILLED]: "autofill",
|
||||
// highlighted && grey color text
|
||||
[FIELD_STATES.PREVIEW]: "-moz-autofill-preview",
|
||||
};
|
||||
constructor(fieldDetails, handler) {
|
||||
this.fieldDetails = fieldDetails;
|
||||
this.handler = handler;
|
||||
this.filledRecordGUID = null;
|
||||
|
||||
if (!this.isValidSection()) {
|
||||
this.fieldDetails = [];
|
||||
|
@ -117,6 +107,15 @@ class FormAutofillSection {
|
|||
throw new TypeError("isRecordCreatable method must be overridden");
|
||||
}
|
||||
|
||||
/*
|
||||
* Override this method if any data for `createRecord` is needed to be
|
||||
* normalized before submitting the record.
|
||||
*
|
||||
* @param {Object} profile
|
||||
* A record for normalization.
|
||||
*/
|
||||
createNormalizedRecord(data) {}
|
||||
|
||||
/**
|
||||
* Override this method if the profile is needed to apply some transformers.
|
||||
*
|
||||
|
@ -146,14 +145,20 @@ class FormAutofillSection {
|
|||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Override this method if any data for `createRecord` is needed to be
|
||||
* normalized before submitting the record.
|
||||
/**
|
||||
* Override this method if the profile is needed to be customized for filling
|
||||
* values.
|
||||
*
|
||||
* @param {Object} profile
|
||||
* A record for normalization.
|
||||
* @param {object} fieldDetail A fieldDetail of the related element.
|
||||
* @param {object} profile The profile to fill.
|
||||
* @returns {string} The value to fill for the given field.
|
||||
*/
|
||||
createNormalizedRecord(data) {}
|
||||
getFilledValueFromProfile(fieldDetail, profile) {
|
||||
return (
|
||||
profile[`${fieldDetail.fieldName}-formatted`] ||
|
||||
profile[fieldDetail.fieldName]
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Override this method if there is any field value needs to compute for a
|
||||
|
@ -173,7 +178,7 @@ class FormAutofillSection {
|
|||
}
|
||||
|
||||
set focusedInput(element) {
|
||||
this._focusedDetail = this.getFieldDetailByElement(element);
|
||||
this.#focusedInput = element;
|
||||
}
|
||||
|
||||
getFieldDetailByElement(element) {
|
||||
|
@ -182,6 +187,10 @@ class FormAutofillSection {
|
|||
);
|
||||
}
|
||||
|
||||
getFieldDetailByName(fieldName) {
|
||||
return this.fieldDetails.find(detail => detail.fieldName == fieldName);
|
||||
}
|
||||
|
||||
get allFieldNames() {
|
||||
if (!this._cacheValue.allFieldNames) {
|
||||
this._cacheValue.allFieldNames = this.fieldDetails.map(
|
||||
|
@ -191,10 +200,6 @@ class FormAutofillSection {
|
|||
return this._cacheValue.allFieldNames;
|
||||
}
|
||||
|
||||
getFieldDetailByName(fieldName) {
|
||||
return this.fieldDetails.find(detail => detail.fieldName == fieldName);
|
||||
}
|
||||
|
||||
matchSelectOptions(profile) {
|
||||
if (!this._cacheValue.matchingSelectOption) {
|
||||
this._cacheValue.matchingSelectOption = new WeakMap();
|
||||
|
@ -321,7 +326,11 @@ class FormAutofillSection {
|
|||
* True if successful, false if failed
|
||||
*/
|
||||
async autofillFields(profile) {
|
||||
let focusedDetail = this._focusedDetail;
|
||||
if (!this.#focusedInput) {
|
||||
throw new Error("No focused input.");
|
||||
}
|
||||
|
||||
const focusedDetail = this.getFieldDetailByElement(this.#focusedInput);
|
||||
if (!focusedDetail) {
|
||||
throw new Error("No fieldDetail for the focused input.");
|
||||
}
|
||||
|
@ -331,16 +340,14 @@ class FormAutofillSection {
|
|||
return false;
|
||||
}
|
||||
|
||||
let focusedInput = focusedDetail.elementWeakRef.get();
|
||||
|
||||
this.filledRecordGUID = profile.guid;
|
||||
for (let fieldDetail of this.fieldDetails) {
|
||||
for (const fieldDetail of this.fieldDetails) {
|
||||
// Avoid filling field value in the following cases:
|
||||
// 1. a non-empty input field for an unfocused input
|
||||
// 2. the invalid value set
|
||||
// 3. value already chosen in select element
|
||||
|
||||
let element = fieldDetail.elementWeakRef.get();
|
||||
const element = fieldDetail.elementWeakRef.get();
|
||||
// Skip the field if it is null or readonly or disabled
|
||||
if (!FormAutofillUtils.isFieldAutofillable(element)) {
|
||||
continue;
|
||||
|
@ -352,14 +359,8 @@ class FormAutofillSection {
|
|||
// For example, autofilling expiration month into an input element will not work as expected if
|
||||
// the month is less than 10, since the input is expected a zero-padded string.
|
||||
// See Bug 1722941 for follow up.
|
||||
let value =
|
||||
profile[`${fieldDetail.fieldName}-formatted`] ||
|
||||
profile[fieldDetail.fieldName];
|
||||
const value = this.getFilledValueFromProfile(fieldDetail, profile);
|
||||
|
||||
// Bug 1688607: The transform function allows us to handle the multiple credit card number fields case
|
||||
if (fieldDetail.transform) {
|
||||
value = fieldDetail.transform(value);
|
||||
}
|
||||
if (HTMLInputElement.isInstance(element) && value) {
|
||||
// For the focused input element, it will be filled with a valid value
|
||||
// anyway.
|
||||
|
@ -367,14 +368,15 @@ class FormAutofillSection {
|
|||
// or their values are equal to the site prefill value
|
||||
// or are the result of an earlier auto-fill.
|
||||
if (
|
||||
element == focusedInput ||
|
||||
(element != focusedInput &&
|
||||
element == this.#focusedInput ||
|
||||
(element != this.#focusedInput &&
|
||||
(!element.value || element.value == element.defaultValue)) ||
|
||||
fieldDetail.state == FIELD_STATES.AUTO_FILLED
|
||||
this.handler.getFilledStateByElement(element) ==
|
||||
FIELD_STATES.AUTO_FILLED
|
||||
) {
|
||||
element.focus({ preventScroll: true });
|
||||
element.setUserInput(value);
|
||||
this._changeFieldState(fieldDetail, FIELD_STATES.AUTO_FILLED);
|
||||
this.handler.changeFieldState(fieldDetail, FIELD_STATES.AUTO_FILLED);
|
||||
}
|
||||
} else if (HTMLSelectElement.isInstance(element)) {
|
||||
let cache = this._cacheValue.matchingSelectOption.get(element) || {};
|
||||
|
@ -397,10 +399,10 @@ class FormAutofillSection {
|
|||
);
|
||||
}
|
||||
// Autofill highlight appears regardless if value is changed or not
|
||||
this._changeFieldState(fieldDetail, FIELD_STATES.AUTO_FILLED);
|
||||
this.handler.changeFieldState(fieldDetail, FIELD_STATES.AUTO_FILLED);
|
||||
}
|
||||
}
|
||||
focusedInput.focus({ preventScroll: true });
|
||||
this.#focusedInput.focus({ preventScroll: true });
|
||||
|
||||
lazy.AutofillTelemetry.recordFormInteractionEvent("filled", this, {
|
||||
profile,
|
||||
|
@ -418,49 +420,81 @@ class FormAutofillSection {
|
|||
previewFormFields(profile) {
|
||||
this.preparePreviewProfile(profile);
|
||||
|
||||
for (let fieldDetail of this.fieldDetails) {
|
||||
for (const fieldDetail of this.fieldDetails) {
|
||||
let element = fieldDetail.elementWeakRef.get();
|
||||
let value =
|
||||
profile[`${fieldDetail.fieldName}-formatted`] ||
|
||||
profile[fieldDetail.fieldName] ||
|
||||
"";
|
||||
|
||||
// Skip the field if it is null or readonly or disabled
|
||||
if (!FormAutofillUtils.isFieldAutofillable(element)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value =
|
||||
profile[`${fieldDetail.fieldName}-formatted`] ||
|
||||
profile[fieldDetail.fieldName] ||
|
||||
"";
|
||||
if (HTMLSelectElement.isInstance(element)) {
|
||||
// Unlike text input, select element is always previewed even if
|
||||
// the option is already selected.
|
||||
if (value) {
|
||||
let cache = this._cacheValue.matchingSelectOption.get(element) || {};
|
||||
let option = cache[value] && cache[value].get();
|
||||
if (option) {
|
||||
value = option.text || "";
|
||||
} else {
|
||||
value = "";
|
||||
}
|
||||
const cache =
|
||||
this._cacheValue.matchingSelectOption.get(element) ?? {};
|
||||
const option = cache[value]?.get();
|
||||
value = option?.text ?? "";
|
||||
}
|
||||
} else if (element.value && element.value != element.defaultValue) {
|
||||
// Skip the field if the user has already entered text and that text is not the site prefilled value.
|
||||
continue;
|
||||
}
|
||||
element.previewValue = value;
|
||||
this._changeFieldState(
|
||||
this.handler.changeFieldState(
|
||||
fieldDetail,
|
||||
value ? FIELD_STATES.PREVIEW : FIELD_STATES.NORMAL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a previously autofilled field in this section
|
||||
*/
|
||||
clearFilled(fieldDetail) {
|
||||
lazy.AutofillTelemetry.recordFormInteractionEvent("filled_modified", this, {
|
||||
fieldName: fieldDetail.fieldName,
|
||||
});
|
||||
|
||||
let isAutofilled = false;
|
||||
const dimFieldDetails = [];
|
||||
for (const fieldDetail of this.fieldDetails) {
|
||||
const element = fieldDetail.elementWeakRef.get();
|
||||
|
||||
if (HTMLSelectElement.isInstance(element)) {
|
||||
// Dim fields are those we don't attempt to revert their value
|
||||
// when clear the target set, such as <select>.
|
||||
dimFieldDetails.push(fieldDetail);
|
||||
} else {
|
||||
isAutofilled |=
|
||||
this.handler.getFilledStateByElement(element) ==
|
||||
FIELD_STATES.AUTO_FILLED;
|
||||
}
|
||||
}
|
||||
if (!isAutofilled) {
|
||||
// Restore the dim fields to initial state as well once we knew
|
||||
// that user had intention to clear the filled form manually.
|
||||
for (const fieldDetail of dimFieldDetails) {
|
||||
// If we can't find a selected option, then we should just reset to the first option's value
|
||||
let element = fieldDetail.elementWeakRef.get();
|
||||
this._resetSelectElementValue(element);
|
||||
this.handler.changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
|
||||
}
|
||||
this.filledRecordGUID = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear preview text and background highlight of all fields.
|
||||
*/
|
||||
clearPreviewedFormFields() {
|
||||
lazy.log.debug("clear previewed fields");
|
||||
|
||||
for (let fieldDetail of this.fieldDetails) {
|
||||
for (const fieldDetail of this.fieldDetails) {
|
||||
let element = fieldDetail.elementWeakRef.get();
|
||||
if (!element) {
|
||||
lazy.log.warn(fieldDetail.fieldName, "is unreachable");
|
||||
|
@ -471,11 +505,14 @@ class FormAutofillSection {
|
|||
|
||||
// We keep the state if this field has
|
||||
// already been auto-filled.
|
||||
if (fieldDetail.state == FIELD_STATES.AUTO_FILLED) {
|
||||
if (
|
||||
this.handler.getFilledStateByElement(element) ==
|
||||
FIELD_STATES.AUTO_FILLED
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
|
||||
this.handler.changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -490,7 +527,10 @@ class FormAutofillSection {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (fieldDetail.state == FIELD_STATES.AUTO_FILLED) {
|
||||
if (
|
||||
this.handler.getFilledStateByElement(element) ==
|
||||
FIELD_STATES.AUTO_FILLED
|
||||
) {
|
||||
if (HTMLInputElement.isInstance(element)) {
|
||||
element.setUserInput("");
|
||||
} else if (HTMLSelectElement.isInstance(element)) {
|
||||
|
@ -501,66 +541,11 @@ class FormAutofillSection {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the state of a field to correspond with different presentations.
|
||||
*
|
||||
* @param {object} fieldDetail
|
||||
* A fieldDetail of which its element is about to update the state.
|
||||
* @param {string} nextState
|
||||
* Used to determine the next state
|
||||
*/
|
||||
_changeFieldState(fieldDetail, nextState) {
|
||||
let element = fieldDetail.elementWeakRef.get();
|
||||
|
||||
if (!element) {
|
||||
lazy.log.warn(
|
||||
fieldDetail.fieldName,
|
||||
"is unreachable while changing state"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!(nextState in this._FIELD_STATE_ENUM)) {
|
||||
lazy.log.warn(
|
||||
fieldDetail.fieldName,
|
||||
"is trying to change to an invalid state"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (fieldDetail.state == nextState) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextStateValue = null;
|
||||
for (let [state, mmStateValue] of Object.entries(this._FIELD_STATE_ENUM)) {
|
||||
// The NORMAL state is simply the absence of other manually
|
||||
// managed states so we never need to add or remove it.
|
||||
if (!mmStateValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state == nextState) {
|
||||
nextStateValue = mmStateValue;
|
||||
} else {
|
||||
this.winUtils.removeManuallyManagedState(element, mmStateValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextStateValue) {
|
||||
this.winUtils.addManuallyManagedState(element, nextStateValue);
|
||||
}
|
||||
|
||||
if (nextState == FIELD_STATES.AUTO_FILLED) {
|
||||
element.addEventListener("input", this, { mozSystemGroup: true });
|
||||
}
|
||||
|
||||
fieldDetail.state = nextState;
|
||||
}
|
||||
|
||||
resetFieldStates() {
|
||||
for (let fieldDetail of this.fieldDetails) {
|
||||
for (const fieldDetail of this.fieldDetails) {
|
||||
const element = fieldDetail.elementWeakRef.get();
|
||||
element.removeEventListener("input", this, { mozSystemGroup: true });
|
||||
this._changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
|
||||
this.handler.changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
|
||||
}
|
||||
this.filledRecordGUID = null;
|
||||
}
|
||||
|
@ -630,7 +615,7 @@ class FormAutofillSection {
|
|||
this._condenseMultipleCCNumberFields(condensedDetails);
|
||||
|
||||
condensedDetails.forEach(detail => {
|
||||
let element = detail.elementWeakRef.get();
|
||||
const element = detail.elementWeakRef.get();
|
||||
// Remove the unnecessary spaces
|
||||
let value = detail.fieldValue ?? (element && element.value.trim());
|
||||
value = this.computeFillingValue(value, detail, element);
|
||||
|
@ -643,7 +628,10 @@ class FormAutofillSection {
|
|||
|
||||
data.record[detail.fieldName] = value;
|
||||
|
||||
if (detail.state == FIELD_STATES.AUTO_FILLED) {
|
||||
if (
|
||||
this.handler.getFilledStateByElement(element) ==
|
||||
FIELD_STATES.AUTO_FILLED
|
||||
) {
|
||||
data.untouchedFields.push(detail.fieldName);
|
||||
}
|
||||
});
|
||||
|
@ -657,70 +645,6 @@ class FormAutofillSection {
|
|||
return data;
|
||||
}
|
||||
|
||||
handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "input": {
|
||||
if (!event.isTrusted) {
|
||||
return;
|
||||
}
|
||||
const target = event.target;
|
||||
const targetFieldDetail = this.getFieldDetailByElement(target);
|
||||
const isCreditCardField = FormAutofillUtils.isCreditCardField(
|
||||
targetFieldDetail.fieldName
|
||||
);
|
||||
|
||||
// If the user manually blanks a credit card field, then
|
||||
// we want the popup to be activated.
|
||||
if (
|
||||
!HTMLSelectElement.isInstance(target) &&
|
||||
isCreditCardField &&
|
||||
target.value === ""
|
||||
) {
|
||||
formFillController.showPopup();
|
||||
}
|
||||
|
||||
if (targetFieldDetail.state == FIELD_STATES.NORMAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._changeFieldState(targetFieldDetail, FIELD_STATES.NORMAL);
|
||||
|
||||
lazy.AutofillTelemetry.recordFormInteractionEvent(
|
||||
"filled_modified",
|
||||
this,
|
||||
{
|
||||
fieldName: targetFieldDetail.fieldName,
|
||||
}
|
||||
);
|
||||
|
||||
let isAutofilled = false;
|
||||
let dimFieldDetails = [];
|
||||
for (const fieldDetail of this.fieldDetails) {
|
||||
const element = fieldDetail.elementWeakRef.get();
|
||||
|
||||
if (HTMLSelectElement.isInstance(element)) {
|
||||
// Dim fields are those we don't attempt to revert their value
|
||||
// when clear the target set, such as <select>.
|
||||
dimFieldDetails.push(fieldDetail);
|
||||
} else {
|
||||
isAutofilled |= fieldDetail.state == FIELD_STATES.AUTO_FILLED;
|
||||
}
|
||||
}
|
||||
if (!isAutofilled) {
|
||||
// Restore the dim fields to initial state as well once we knew
|
||||
// that user had intention to clear the filled form manually.
|
||||
for (const fieldDetail of dimFieldDetails) {
|
||||
// If we can't find a selected option, then we should just reset to the first option's value
|
||||
let element = fieldDetail.elementWeakRef.get();
|
||||
this._resetSelectElementValue(element);
|
||||
this._changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
|
||||
}
|
||||
this.filledRecordGUID = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Resets a <select> element to its selected option or the first option if there is none selected.
|
||||
*
|
||||
|
@ -745,8 +669,8 @@ class FormAutofillSection {
|
|||
}
|
||||
|
||||
class FormAutofillAddressSection extends FormAutofillSection {
|
||||
constructor(fieldDetails, winUtils) {
|
||||
super(fieldDetails, winUtils);
|
||||
constructor(fieldDetails, handler) {
|
||||
super(fieldDetails, handler);
|
||||
|
||||
this._cacheValue.oneLineStreetAddress = null;
|
||||
|
||||
|
@ -980,15 +904,11 @@ export class FormAutofillCreditCardSection extends FormAutofillSection {
|
|||
*
|
||||
* @param {object} fieldDetails
|
||||
* The fieldDetail objects for the fields in this section
|
||||
* @param {object} winUtils
|
||||
* A WindowUtils reference for the Window the section appears in
|
||||
* @param {object} handler
|
||||
* The FormAutofillHandler responsible for this section
|
||||
*/
|
||||
constructor(fieldDetails, winUtils, handler) {
|
||||
super(fieldDetails, winUtils);
|
||||
|
||||
this.handler = handler;
|
||||
constructor(fieldDetails, handler) {
|
||||
super(fieldDetails, handler);
|
||||
|
||||
if (!this.isValidSection()) {
|
||||
return;
|
||||
|
@ -1314,6 +1234,15 @@ export class FormAutofillCreditCardSection extends FormAutofillSection {
|
|||
this.adaptFieldMaxLength(profile);
|
||||
}
|
||||
|
||||
getFilledValueFromProfile(fieldDetail, profile) {
|
||||
const value = super.getFilledValueFromProfile(fieldDetail, profile);
|
||||
if (fieldDetail.fieldName == "cc-number" && fieldDetail.part != null) {
|
||||
const part = fieldDetail.part;
|
||||
return value.slice((part - 1) * 4, part * 4);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
computeFillingValue(value, fieldDetail, element) {
|
||||
if (
|
||||
fieldDetail.fieldName != "cc-type" ||
|
||||
|
@ -1443,6 +1372,8 @@ export class FormAutofillHandler {
|
|||
// Caches the element to section mapping
|
||||
#cachedSectionByElement = new WeakMap();
|
||||
|
||||
// Keeps track of filled state for all identified elements
|
||||
#filledStateByElement = new WeakMap();
|
||||
/**
|
||||
* Array of collected data about relevant form fields. Each item is an object
|
||||
* storing the identifying details of the field and a reference to the
|
||||
|
@ -1475,6 +1406,16 @@ export class FormAutofillHandler {
|
|||
this.window = this.form.rootElement.ownerGlobal;
|
||||
this.winUtils = this.window.windowUtils;
|
||||
|
||||
// Enum for form autofill MANUALLY_MANAGED_STATES values
|
||||
this.FIELD_STATE_ENUM = {
|
||||
// not themed
|
||||
[FIELD_STATES.NORMAL]: null,
|
||||
// highlighted
|
||||
[FIELD_STATES.AUTO_FILLED]: "autofill",
|
||||
// highlighted && grey color text
|
||||
[FIELD_STATES.PREVIEW]: "-moz-autofill-preview",
|
||||
};
|
||||
|
||||
/**
|
||||
* This function is used if the form handler (or one of its sections)
|
||||
* determines that it needs to act as if the form had been submitted.
|
||||
|
@ -1484,19 +1425,73 @@ export class FormAutofillHandler {
|
|||
};
|
||||
}
|
||||
|
||||
handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "input": {
|
||||
if (!event.isTrusted) {
|
||||
return;
|
||||
}
|
||||
const target = event.target;
|
||||
const targetFieldDetail = this.getFieldDetailByElement(target);
|
||||
const isCreditCardField = FormAutofillUtils.isCreditCardField(
|
||||
targetFieldDetail.fieldName
|
||||
);
|
||||
|
||||
// If the user manually blanks a credit card field, then
|
||||
// we want the popup to be activated.
|
||||
if (
|
||||
!HTMLSelectElement.isInstance(target) &&
|
||||
isCreditCardField &&
|
||||
target.value === ""
|
||||
) {
|
||||
formFillController.showPopup();
|
||||
}
|
||||
|
||||
if (this.getFilledStateByElement(target) == FIELD_STATES.NORMAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.changeFieldState(targetFieldDetail, FIELD_STATES.NORMAL);
|
||||
const section = this.getSectionByElement(
|
||||
targetFieldDetail.elementWeakRef.get()
|
||||
);
|
||||
section?.clearFilled(targetFieldDetail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set focusedInput(element) {
|
||||
const section =
|
||||
this.#cachedSectionByElement.get(element) ??
|
||||
this.sections.find(s => s.getFieldDetailByElement(element));
|
||||
const section = this.getSectionByElement(element);
|
||||
if (!section) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#cachedSectionByElement.set(element, section);
|
||||
this.#focusedSection = section;
|
||||
this.#focusedSection.focusedInput = element;
|
||||
}
|
||||
|
||||
getSectionByElement(element) {
|
||||
const section =
|
||||
this.#cachedSectionByElement.get(element) ??
|
||||
this.sections.find(s => s.getFieldDetailByElement(element));
|
||||
if (!section) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.#cachedSectionByElement.set(element, section);
|
||||
return section;
|
||||
}
|
||||
|
||||
getFieldDetailByElement(element) {
|
||||
for (const section of this.sections) {
|
||||
const detail = section.getFieldDetailByElement(element);
|
||||
if (detail) {
|
||||
return detail;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get activeSection() {
|
||||
return this.#focusedSection;
|
||||
}
|
||||
|
@ -1567,13 +1562,9 @@ export class FormAutofillHandler {
|
|||
for (const { fieldDetails, type } of sections) {
|
||||
let section;
|
||||
if (type == FormAutofillUtils.SECTION_TYPES.ADDRESS) {
|
||||
section = new FormAutofillAddressSection(fieldDetails, this.winUtils);
|
||||
section = new FormAutofillAddressSection(fieldDetails, this);
|
||||
} else if (type == FormAutofillUtils.SECTION_TYPES.CREDIT_CARD) {
|
||||
section = new FormAutofillCreditCardSection(
|
||||
fieldDetails,
|
||||
this.winUtils,
|
||||
this
|
||||
);
|
||||
section = new FormAutofillCreditCardSection(fieldDetails, this);
|
||||
} else {
|
||||
throw new Error("Unknown field type.");
|
||||
}
|
||||
|
@ -1589,6 +1580,65 @@ export class FormAutofillHandler {
|
|||
return this.sections.some(section => section.isFilled());
|
||||
}
|
||||
|
||||
getFilledStateByElement(element) {
|
||||
return this.#filledStateByElement.get(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the state of a field to correspond with different presentations.
|
||||
*
|
||||
* @param {object} fieldDetail
|
||||
* A fieldDetail of which its element is about to update the state.
|
||||
* @param {string} nextState
|
||||
* Used to determine the next state
|
||||
*/
|
||||
changeFieldState(fieldDetail, nextState) {
|
||||
const element = fieldDetail.elementWeakRef.get();
|
||||
if (!element) {
|
||||
lazy.log.warn(
|
||||
fieldDetail.fieldName,
|
||||
"is unreachable while changing state"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!(nextState in this.FIELD_STATE_ENUM)) {
|
||||
lazy.log.warn(
|
||||
fieldDetail.fieldName,
|
||||
"is trying to change to an invalid state"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#filledStateByElement.get(element) == nextState) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextStateValue = null;
|
||||
for (const [state, mmStateValue] of Object.entries(this.FIELD_STATE_ENUM)) {
|
||||
// The NORMAL state is simply the absence of other manually
|
||||
// managed states so we never need to add or remove it.
|
||||
if (!mmStateValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state == nextState) {
|
||||
nextStateValue = mmStateValue;
|
||||
} else {
|
||||
this.winUtils.removeManuallyManagedState(element, mmStateValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextStateValue) {
|
||||
this.winUtils.addManuallyManagedState(element, nextStateValue);
|
||||
}
|
||||
|
||||
if (nextState == FIELD_STATES.AUTO_FILLED) {
|
||||
element.addEventListener("input", this, { mozSystemGroup: true });
|
||||
}
|
||||
|
||||
this.#filledStateByElement.set(element, nextState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes form fields that can be autofilled, and populates them with the
|
||||
* profile provided by backend.
|
||||
|
|
|
@ -75,6 +75,12 @@ export class FieldDetail {
|
|||
addressType = "";
|
||||
contactType = "";
|
||||
|
||||
// When a field is split into N fields, we use part to record which field it is
|
||||
// For example, a credit card number field is split into 4 fields, the value of
|
||||
// "part" for the first cc-number field is 1, for the last one is 4.
|
||||
// If the field is not split, the value is null
|
||||
part = null;
|
||||
|
||||
// Confidence value when the field name is inferred by "fathom"
|
||||
confidence = null;
|
||||
|
||||
|
@ -98,6 +104,16 @@ export class FieldDetail {
|
|||
this.reason = "regex-heuristic";
|
||||
}
|
||||
}
|
||||
|
||||
isSame(other) {
|
||||
return (
|
||||
this.fieldName == other.fieldName &&
|
||||
this.section == other.section &&
|
||||
this.addressType == other.addressType &&
|
||||
!this.part &&
|
||||
!other.part
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -389,16 +405,6 @@ export class FieldScanner {
|
|||
this.fieldDetails[index].fieldName = fieldName;
|
||||
}
|
||||
|
||||
#isSameField(field1, field2) {
|
||||
return (
|
||||
field1.section == field2.section &&
|
||||
field1.addressType == field2.addressType &&
|
||||
field1.fieldName == field2.fieldName &&
|
||||
!field1.transform &&
|
||||
!field2.transform
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* When a site has four credit card number fields and
|
||||
* these fields have a max length of four
|
||||
|
@ -410,17 +416,18 @@ export class FieldScanner {
|
|||
* @memberof FieldScanner
|
||||
*/
|
||||
#transformCCNumberForMultipleFields(creditCardFieldDetails) {
|
||||
const ccNumberFields = creditCardFieldDetails.filter(
|
||||
const details = creditCardFieldDetails.filter(
|
||||
field =>
|
||||
field.fieldName == "cc-number" &&
|
||||
field.elementWeakRef.get().maxLength == 4
|
||||
);
|
||||
if (ccNumberFields.length == 4) {
|
||||
ccNumberFields[0].transform = fullCCNumber => fullCCNumber.slice(0, 4);
|
||||
ccNumberFields[1].transform = fullCCNumber => fullCCNumber.slice(4, 8);
|
||||
ccNumberFields[2].transform = fullCCNumber => fullCCNumber.slice(8, 12);
|
||||
ccNumberFields[3].transform = fullCCNumber => fullCCNumber.slice(12, 16);
|
||||
if (details.length != 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
details.map((detail, idx) => {
|
||||
detail.part = idx + 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -454,6 +461,7 @@ export class FieldScanner {
|
|||
);
|
||||
}
|
||||
}
|
||||
dump(`[Dimi]getFinalDetails: ${JSON.stringify(addressFieldDetails)}\n`);
|
||||
this.#transformCCNumberForMultipleFields(creditCardFieldDetails);
|
||||
return [
|
||||
{
|
||||
|
@ -470,8 +478,9 @@ export class FieldScanner {
|
|||
const details = section.fieldDetails;
|
||||
section.fieldDetails = details.filter((detail, index) => {
|
||||
const previousFields = details.slice(0, index);
|
||||
return !previousFields.find(f => this.#isSameField(detail, f));
|
||||
return !previousFields.find(f => f.isSame(detail));
|
||||
});
|
||||
dump(`[Dimi]section:${JSON.stringify(section.fieldDetails)}\n`);
|
||||
return section;
|
||||
})
|
||||
.filter(section => !!section.fieldDetails.length);
|
||||
|
|
Загрузка…
Ссылка в новой задаче