diff --git a/apps/browser/src/autofill/services/abstractions/inline-menu-field-qualifications.service.ts b/apps/browser/src/autofill/services/abstractions/inline-menu-field-qualifications.service.ts new file mode 100644 index 00000000000..c8303c0f81b --- /dev/null +++ b/apps/browser/src/autofill/services/abstractions/inline-menu-field-qualifications.service.ts @@ -0,0 +1,6 @@ +import AutofillField from "../../models/autofill-field"; +import AutofillPageDetails from "../../models/autofill-page-details"; + +export interface InlineMenuFieldQualificationsService { + isFieldForLoginForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean; +} diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts index f0601b31f53..6b8cb91a164 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts @@ -184,7 +184,12 @@ describe("AutofillOverlayContentService", () => { describe("skips setup for ignored form fields", () => { beforeEach(() => { - autofillFieldData = mock(); + autofillFieldData = mock({ + type: "text", + htmlName: "username", + htmlID: "username", + placeholder: "username", + }); }); it("ignores fields that are readonly", async () => { diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 6033331257a..d56a8a80cc6 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -97,7 +97,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte ) { if ( this.formFieldElements.has(formFieldElement) || - !this.inlineMenuFieldQualificationService.isFieldForLoginForm(autofillFieldData, pageDetails) + this.isIgnoredField(autofillFieldData, pageDetails) ) { return; } @@ -700,6 +700,33 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte }); } + /** + * Identifies if the field should have the autofill overlay setup on it. Currently, this is mainly + * determined by whether the field correlates with a login cipher. This method will need to be + * updated in the future to support other types of forms. + * + * @param autofillFieldData - Autofill field data captured from the form field element. + * @param pageDetails - The collected page details from the tab. + */ + private isIgnoredField( + autofillFieldData: AutofillField, + pageDetails: AutofillPageDetails, + ): boolean { + if ( + autofillFieldData.readonly || + autofillFieldData.disabled || + !autofillFieldData.viewable || + this.ignoredFieldTypes.has(autofillFieldData.type) + ) { + return true; + } + + return !this.inlineMenuFieldQualificationService.isFieldForLoginForm( + autofillFieldData, + pageDetails, + ); + } + /** * Creates the autofill overlay button element. Will not attempt * to create the element if it already exists in the DOM. diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts new file mode 100644 index 00000000000..c0b2b9c8a30 --- /dev/null +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts @@ -0,0 +1,50 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import AutofillField from "../models/autofill-field"; +import AutofillPageDetails from "../models/autofill-page-details"; + +import { AutoFillConstants } from "./autofill-constants"; +import { InlineMenuFieldQualificationService } from "./inline-menu-field-qualification.service"; + +describe("InlineMenuFieldQualificationService", () => { + let pageDetails: MockProxy; + let inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; + + beforeEach(() => { + pageDetails = mock(); + inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); + }); + + describe("isFieldForLoginForm", () => { + describe("validating a password field for a login form", () => { + describe("an invalid password field", () => { + it("has a `new-password` autoCompleteType", () => { + const newPasswordField = mock({ + type: "password", + autoCompleteType: "new-password", + }); + + expect( + inlineMenuFieldQualificationService.isFieldForLoginForm(newPasswordField, pageDetails), + ).toBe(false); + }); + + it("has a type that is an excluded type", () => { + AutoFillConstants.ExcludedAutofillLoginTypes.forEach((excludedType) => { + const excludedField = mock({ + type: excludedType, + }); + + expect( + inlineMenuFieldQualificationService.isFieldForLoginForm(excludedField, pageDetails), + ).toBe(false); + }); + }); + }); + + describe("a valid password field", () => {}); + }); + + describe("validating a username field for a login form", () => {}); + }); +}); diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index fc1d4c7071f..11f23fad9bb 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -1,9 +1,12 @@ import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; +import { InlineMenuFieldQualificationsService as InlineMenuFieldQualificationsServiceInterface } from "./abstractions/inline-menu-field-qualifications.service"; import { AutoFillConstants } from "./autofill-constants"; -export class InlineMenuFieldQualificationService { +export class InlineMenuFieldQualificationService + implements InlineMenuFieldQualificationsServiceInterface +{ private searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames); private excludedAutofillLoginTypesSet = new Set(AutoFillConstants.ExcludedAutofillLoginTypes); private usernameFieldTypes = new Set(["text", "email", "number", "tel"]); @@ -14,6 +17,12 @@ export class InlineMenuFieldQualificationService { private autocompleteDisabledValues = new Set(["off", "false"]); private newFieldKeywords = new Set(["new", "change", "neue", "ändern"]); + /** + * Validates the provided field as a field for a login form. + * + * @param field - The field to validate, should be a username or password field. + * @param pageDetails - The details of the page that the field is on. + */ isFieldForLoginForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean { const isCurrentPasswordField = this.isCurrentPasswordField(field); if (isCurrentPasswordField) { @@ -28,6 +37,12 @@ export class InlineMenuFieldQualificationService { return this.isUsernameFieldForLoginForm(field, pageDetails); } + /** + * Validates the provided field as a password field for a login form. + * + * @param field - The field to validate + * @param pageDetails - The details of the page that the field is on. + */ private isPasswordFieldForLoginForm( field: AutofillField, pageDetails: AutofillPageDetails, @@ -91,6 +106,12 @@ export class InlineMenuFieldQualificationService { return !this.autocompleteDisabledValues.has(field.autoCompleteType); } + /** + * Validates the provided field as a username field for a login form. + * + * @param field - The field to validate + * @param pageDetails - The details of the page that the field is on. + */ private isUsernameFieldForLoginForm( field: AutofillField, pageDetails: AutofillPageDetails, @@ -171,7 +192,12 @@ export class InlineMenuFieldQualificationService { return visiblePasswordFieldsInPageDetails.length === 1; } - isUsernameField = (field: AutofillField): boolean => { + /** + * Validates the provided field as a username field. + * + * @param field - The field to validate + */ + private isUsernameField = (field: AutofillField): boolean => { if ( !this.usernameFieldTypes.has(field.type) || this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet) @@ -182,7 +208,12 @@ export class InlineMenuFieldQualificationService { return this.keywordsFoundInFieldData(field, AutoFillConstants.UsernameFieldNames); }; - isCurrentPasswordField = (field: AutofillField): boolean => { + /** + * Validates the provided field as a current password field. + * + * @param field - The field to validate + */ + private isCurrentPasswordField = (field: AutofillField): boolean => { if (field.autoCompleteType === "new-password") { return false; } @@ -190,7 +221,12 @@ export class InlineMenuFieldQualificationService { return this.isPasswordField(field); }; - isNewPasswordField = (field: AutofillField): boolean => { + /** + * Validates the provided field as a new password field. + * + * @param field - The field to validate + */ + private isNewPasswordField = (field: AutofillField): boolean => { if (field.autoCompleteType === "current-password") { return false; } @@ -198,11 +234,16 @@ export class InlineMenuFieldQualificationService { return this.isPasswordField(field); }; - isPasswordField = (field: AutofillField): boolean => { + /** + * Validates the provided field as a password field. + * + * @param field - The field to validate + */ + private isPasswordField = (field: AutofillField): boolean => { const isInputPasswordType = field.type === "password"; if ( - !isInputPasswordType || - this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet) || + (!isInputPasswordType && + this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet)) || this.fieldHasDisqualifyingAttributeValue(field) ) { return false; @@ -211,6 +252,12 @@ export class InlineMenuFieldQualificationService { return isInputPasswordType || this.isLikePasswordField(field); }; + /** + * Validates the provided field as a field to indicate if the + * field potentially acts as a password field. + * + * @param field - The field to validate + */ private isLikePasswordField(field: AutofillField): boolean { if (field.type !== "text") { return false; @@ -226,6 +273,11 @@ export class InlineMenuFieldQualificationService { return false; } + /** + * Validates the provided value to indicate if the value is like a password. + * + * @param value - The value to validate + */ private valueIsLikePassword(value: string): boolean { if (value == null) { return false; @@ -240,6 +292,12 @@ export class InlineMenuFieldQualificationService { return !(this.passwordFieldExcludeListString.indexOf(cleanedValue) > -1); } + /** + * Validates the provided field to indicate if the field has a + * disqualifying attribute that would impede autofill entirely. + * + * @param field - The field to validate + */ private fieldHasDisqualifyingAttributeValue(field: AutofillField): boolean { const checkedAttributeValues = [field.htmlID, field.htmlName, field.placeholder]; @@ -255,6 +313,12 @@ export class InlineMenuFieldQualificationService { return false; } + /** + * Validates the provided field to indicate if the field is excluded from autofill. + * + * @param field - The field to validate + * @param excludedTypes - The set of excluded types + */ private isExcludedFieldType(field: AutofillField, excludedTypes: Set): boolean { if (excludedTypes.has(field.type)) { return true; @@ -263,6 +327,11 @@ export class InlineMenuFieldQualificationService { return this.isSearchField(field); } + /** + * Validates the provided field to indicate if the field is a search field. + * + * @param field - The field to validate + */ private isSearchField(field: AutofillField): boolean { const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder]; for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) { @@ -287,11 +356,22 @@ export class InlineMenuFieldQualificationService { return false; } + /** + * Validates the provided field to indicate if the field has any of the provided keywords. + * + * @param autofillFieldData - The field data to search for keywords + * @param keywords - The keywords to search for + */ private keywordsFoundInFieldData(autofillFieldData: AutofillField, keywords: string[]) { const searchedString = this.getAutofillFieldDataKeywords(autofillFieldData); return keywords.some((keyword) => searchedString.includes(keyword)); } + /** + * Retrieves the keywords from the provided autofill field data. + * + * @param autofillFieldData - The field data to search for keywords + */ private getAutofillFieldDataKeywords(autofillFieldData: AutofillField) { if (this.autofillFieldKeywordsMap.has(autofillFieldData)) { return this.autofillFieldKeywordsMap.get(autofillFieldData);