Skip to content

Commit

Permalink
Refactor [vXXX] auto update credential provider script
Browse files Browse the repository at this point in the history
  • Loading branch information
github-actions[bot] authored Jul 27, 2024
1 parent 05e7312 commit 61a74ae
Show file tree
Hide file tree
Showing 11 changed files with 1,478 additions and 1,785 deletions.
286 changes: 81 additions & 205 deletions firefox-ios/Client/Assets/CC_Script/AutofillTelemetry.sys.mjs

Large diffs are not rendered by default.

77 changes: 59 additions & 18 deletions firefox-ios/Client/Assets/CC_Script/FieldScanner.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,21 @@ export class FieldDetail {
// Reference to the elemenet
elementWeakRef = null;

// id/name. This is only used for debugging
// The identifier generated via ContentDOMReference for the associated DOM element
// of this field
elementId = null;

// The identifier generated via ContentDOMReference for the root element of
// this field
rootElementId = null;

// string with `${element.id}/{element.name}`. This is only used for debugging.
identifier = "";

// The inferred field name for this element
// tag name attribute of the element
tagName = null;

// The inferred field name for this element.
fieldName = null;

// The approach we use to infer the information for this element
Expand Down Expand Up @@ -50,11 +61,17 @@ export class FieldDetail {

constructor(
element,
form,
fieldName = null,
{ autocompleteInfo = {}, confidence = null } = {}
) {
this.elementWeakRef = new WeakRef(element);
this.elementId = lazy.FormAutofillUtils.getElementIdentifier(element);
this.rootElementId = lazy.FormAutofillUtils.getElementIdentifier(
form.rootElement
);
this.identifier = `${element.id}/${element.name}`;
this.tagName = element.tagName;
this.fieldName = fieldName;

if (autocompleteInfo) {
Expand All @@ -63,28 +80,45 @@ export class FieldDetail {
this.addressType = autocompleteInfo.addressType;
this.contactType = autocompleteInfo.contactType;
this.credentialType = autocompleteInfo.credentialType;
this.sectionName = this.section || this.addressType;
} else if (confidence) {
this.reason = "fathom";
this.confidence = confidence;

// TODO: This should be removed once we support reference field info across iframe.
// Temporarily add an addtional "the field is the only visible input" constraint
// when determining whether a form has only a high-confidence cc-* field a valid
// credit card section. We can remove this restriction once we are confident
// about only using fathom.
this.isOnlyVisibleFieldWithHighConfidence = false;
if (
this.confidence > lazy.FormAutofillUtils.ccFathomHighConfidenceThreshold
) {
const root = element.form || element.ownerDocument;
const inputs = root.querySelectorAll("input:not([type=hidden])");
if (inputs.length == 1 && inputs[0] == element) {
this.isOnlyVisibleFieldWithHighConfidence = true;
}
}
} else {
this.reason = "regex-heuristic";
}

this.isVisible = lazy.FormAutofillUtils.isFieldVisible(this.element);
}

get element() {
return this.elementWeakRef.deref();
}

get sectionName() {
return this.section || this.addressType;
}

#isVisible = null;
get isVisible() {
if (this.#isVisible == null) {
this.#isVisible = lazy.FormAutofillUtils.isFieldVisible(this.element);
}
return this.#isVisible;
/**
* Convert FieldDetail class to an object that is suitable for
* sending over IPC. Avoid using this in other case.
*/
toVanillaObject() {
const json = { ...this };
delete json.elementWeakRef;
return json;
}
}

Expand All @@ -96,6 +130,7 @@ export class FieldDetail {
* `inferFieldInfo` function.
*/
export class FieldScanner {
#form = null;
#elementsWeakRef = null;
#inferFieldInfoFn = null;

Expand All @@ -107,12 +142,16 @@ export class FieldScanner {
* Create a FieldScanner based on form elements with the existing
* fieldDetails.
*
* @param {Array.DOMElement} elements
* The elements from a form for each parser.
* @param {FormLike} form
* @param {Funcion} inferFieldInfoFn
* The callback function that is used to infer the field info of a given element
*/
constructor(elements, inferFieldInfoFn) {
constructor(form, inferFieldInfoFn) {
const elements = Array.from(form.elements).filter(element =>
lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element)
);

this.#form = form;
this.#elementsWeakRef = new WeakRef(elements);
this.#inferFieldInfoFn = inferFieldInfoFn;
}
Expand Down Expand Up @@ -189,9 +228,11 @@ export class FieldScanner {
throw new Error("Try to push the non-existing element info.");
}
const element = this.#elements[elementIndex];
const [fieldName, autocompleteInfo, confidence] =
this.#inferFieldInfoFn(element);
const fieldDetail = new FieldDetail(element, fieldName, {
const [fieldName, autocompleteInfo, confidence] = this.#inferFieldInfoFn(
element,
this.#elements
);
const fieldDetail = new FieldDetail(element, this.#form, fieldName, {
autocompleteInfo,
confidence,
});
Expand Down
153 changes: 107 additions & 46 deletions firefox-ios/Client/Assets/CC_Script/FormAutofillChild.ios.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* eslint-disable no-undef,mozilla/balanced-listeners */
import { AddressRecord } from "resource://gre/modules/shared/AddressRecord.sys.mjs";
import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs";
import { FormStateManager } from "resource://gre/modules/shared/FormStateManager.sys.mjs";
import { CreditCardRecord } from "resource://gre/modules/shared/CreditCardRecord.sys.mjs";
import { AddressRecord } from "resource://gre/modules/shared/AddressRecord.sys.mjs";
import {
FormAutofillAddressSection,
FormAutofillCreditCardSection,
FormAutofillSection,
} from "resource://gre/modules/shared/FormAutofillSection.sys.mjs";

export class FormAutofillChild {
/**
Expand All @@ -28,34 +33,11 @@ export class FormAutofillChild {

this.fieldDetailsManager = new FormStateManager();

document.addEventListener("focusin", this.onFocusIn);
document.addEventListener("submit", this.onSubmit);
}

_doIdentifyAutofillFields(element) {
this.fieldDetailsManager.updateActiveInput(element);
this.fieldDetailsManager.identifyAutofillFields(element);

const activeFieldName =
this.fieldDetailsManager.activeFieldDetail?.fieldName;

const activeFieldDetails =
this.fieldDetailsManager.activeSection?.fieldDetails;

// Only ping swift if current field is either a cc or address field
if (!activeFieldDetails?.find(field => field.element === element)) {
return;
}

const fieldNamesWithValues =
this.transformToFieldNamesWithValues(activeFieldDetails);
if (FormAutofillUtils.isAddressField(activeFieldName)) {
this.callbacks.address.autofill(fieldNamesWithValues);
} else if (FormAutofillUtils.isCreditCardField(activeFieldName)) {
// Normalize record format so we always get a consistent
// credit card record format: {cc-number, cc-name, cc-exp-month, cc-exp-year}
CreditCardRecord.normalizeFields(fieldNamesWithValues);
this.callbacks.creditCard.autofill(fieldNamesWithValues);
try {
document.addEventListener("focusin", this.onFocusIn);
document.addEventListener("submit", this.onSubmit);
} catch {
// We don't have `document` when running in xpcshell-test
}
}

Expand All @@ -69,50 +51,129 @@ export class FormAutofillChild {
);
}

onFocusIn(evt) {
const element = evt.target;
this.fieldDetailsManager.updateActiveInput(element);
_doIdentifyAutofillFields(element) {
if (this.#focusedElement == element) {
return;
}
this.#focusedElement = element;

if (!FormAutofillUtils.isCreditCardOrAddressFieldType(element)) {
return;
}

// Find the autofill handler for this form and identify all the fields.
const { handler, newFieldsIdentified } =
this.fieldDetailsManager.identifyAutofillFields(element);

// If we found newly identified fields, run section classification heuristic
if (newFieldsIdentified) {
this.#sections = FormAutofillSection.classifySections(
handler.fieldDetails
);
}
}

#focusedElement = null;

// This is a cache contains the classified section for the active form.
#sections = null;

get activeSection() {
const elementId = this.activeFieldDetail?.elementId;
return this.#sections?.find(section =>
section.getFieldDetailByElementId(elementId)
);
}

// active field detail only exists if we identified its field name
get activeFieldDetail() {
return this.activeHandler?.getFieldDetailByElement(this.#focusedElement);
}

get activeHandler() {
return this.fieldDetailsManager.getFormHandler(this.#focusedElement);
}

onFocusIn(evt) {
const element = evt.target;

this._doIdentifyAutofillFields(element);

// Only ping swift if current field is either a cc or address field
if (!this.activeFieldDetail) {
return;
}

const fieldNamesWithValues = this.transformToFieldNamesWithValues(
this.activeSection.fieldDetails
);

if (FormAutofillUtils.isAddressField(this.activeFieldDetail.fieldName)) {
this.callbacks.address.autofill(fieldNamesWithValues);
} else if (
FormAutofillUtils.isCreditCardField(this.activeFieldDetail.fieldName)
) {
// Normalize record format so we always get a consistent
// credit card record format: {cc-number, cc-name, cc-exp-month, cc-exp-year}
CreditCardRecord.normalizeFields(fieldNamesWithValues);
this.callbacks.creditCard.autofill(fieldNamesWithValues);
}
}

onSubmit(_event) {
if (!this.fieldDetailsManager.activeHandler) {
if (!this.activeHandler) {
return;
}

this.fieldDetailsManager.activeHandler.onFormSubmitted();
const records = this.fieldDetailsManager.activeHandler.createRecords();
// Get filled value for the form
const formFilledData = this.activeHandler.collectFormFilledData();

// Should reference `_onFormSubmit` in `FormAutofillParent.sys.mjs`
const creditCard = [];

for (const section of this.#sections) {
const secRecord = section.createRecord(formFilledData);
if (!secRecord) {
continue;
}

if (section instanceof FormAutofillAddressSection) {
// TODO(FXSP-133 Phase 3): Support address capture
// this.callbacks.address.submit();
continue;
} else if (section instanceof FormAutofillCreditCardSection) {
creditCard.push(secRecord);
} else {
throw new Error("Unknown section type");
}
}

if (records.creditCard.length) {
if (creditCard.length) {
// Normalize record format so we always get a consistent
// credit card record format: {cc-number, cc-name, cc-exp-month, cc-exp-year}
const creditCardRecords = records.creditCard.map(entry => {
const creditCardRecords = creditCard.map(entry => {
CreditCardRecord.normalizeFields(entry.record);
return entry.record;
});
this.callbacks.creditCard.submit(creditCardRecords);
}

// TODO(FXSP-133 Phase 3): Support address capture
// this.callbacks.address.submit();
}

fillFormFields(payload) {
// In iOS, we have access only to valid fields (https://github.com/mozilla/application-services/blob/9054db4bb5031881550ceab3448665ef6499a706/components/autofill/src/autofill.udl#L59-L76) for an address;
// all additional data must be computed. On Desktop, computed fields are handled in FormAutofillStorageBase.sys.mjs at the time of saving. Ideally, we should centralize
// all transformations, computations, and normalization processes within AddressRecord.sys.mjs to maintain a unified implementation across both platforms.
// This will be addressed in FXCM-810, aiming to simplify our data representation for both credit cards and addresses.
if (
FormAutofillUtils.isAddressField(
this.fieldDetailsManager.activeFieldDetail?.fieldName
)
) {

if (FormAutofillUtils.isAddressField(this.activeFieldDetail?.fieldName)) {
AddressRecord.computeFields(payload);
}
this.fieldDetailsManager.activeHandler.autofillFormFields(payload);

this.activeHandler.fillFields(
FormAutofillUtils.getElementIdentifier(this.#focusedElement),
this.activeSection.fieldDetails.map(f => f.elementId),
payload
);
}
}

Expand Down
Loading

0 comments on commit 61a74ae

Please sign in to comment.