From 6b9564db8f259ac8cf7e066fd3dd2a8da458e6f1 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Thu, 9 May 2024 17:27:04 -0500 Subject: [PATCH] [PM-8027] Inlin menu appears within input fields that do not relate to user login --- .../collect-autofill-content.service.ts | 2 + .../autofill-field-qualification.service.ts | 116 ++++++++++++++++++ .../collect-autofill-content.service.ts | 20 +-- .../implementations/inline-derived-state.ts | 38 ++++++ 4 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 apps/browser/src/autofill/services/autofill-field-qualification.service.ts create mode 100644 libs/common/src/platform/state/implementations/inline-derived-state.ts diff --git a/apps/browser/src/autofill/services/abstractions/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/abstractions/collect-autofill-content.service.ts index 46ad615059a..1d705618b75 100644 --- a/apps/browser/src/autofill/services/abstractions/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/abstractions/collect-autofill-content.service.ts @@ -15,6 +15,8 @@ type UpdateAutofillDataAttributeParams = { }; interface CollectAutofillContentService { + autofillFormElements: AutofillFormElements; + autofillFieldElements: AutofillFieldElements; getPageDetails(): Promise; getAutofillFieldElementByOpid(opid: string): HTMLElement | null; queryAllTreeWalkerNodes( diff --git a/apps/browser/src/autofill/services/autofill-field-qualification.service.ts b/apps/browser/src/autofill/services/autofill-field-qualification.service.ts new file mode 100644 index 00000000000..aa32b389bac --- /dev/null +++ b/apps/browser/src/autofill/services/autofill-field-qualification.service.ts @@ -0,0 +1,116 @@ +import AutofillField from "../models/autofill-field"; +import AutofillPageDetails from "../models/autofill-page-details"; + +import { AutoFillConstants } from "./autofill-constants"; + +export class AutofillFieldQualificationService { + autofillPageDetails: AutofillPageDetails; + private searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames); + private excludedAutofillLoginTypesSet = new Set(AutoFillConstants.ExcludedAutofillLoginTypes); + private fieldIgnoreListString = AutoFillConstants.FieldIgnoreList.join(","); + private passwordFieldExcludeListString = AutoFillConstants.PasswordFieldExcludeList.join(","); + + setAutofillPageDetails(autofillPageDetails: AutofillPageDetails): void { + this.autofillPageDetails = autofillPageDetails; + } + + isLoginUsernameField(field: AutofillField): boolean { + return false; + } + + isLoginPasswordField(field: AutofillField): boolean { + if ( + field.type !== "password" || + field.autoComplete === "new-password" || + this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet) + ) { + return false; + } + + if (this.fieldHasDisqualifyingAttributeValue(field)) { + return false; + } + + if (field.type === "password") { + return true; + } + + return this.isPseudoPasswordField(field); + } + + private isPseudoPasswordField(field: AutofillField): boolean { + if (field.type !== "text") { + return false; + } + + const testedValues = [field.htmlID, field.htmlName, field.placeholder]; + for (let i = 0; i < testedValues.length; i++) { + if (this.valueIsLikePassword(testedValues[i])) { + return true; + } + } + + return false; + } + + private valueIsLikePassword(value: string): boolean { + if (value == null) { + return false; + } + // Removes all whitespace, _ and - characters + const cleanedValue = value.toLowerCase().replace(/[\s_-]/g, ""); + + if (cleanedValue.indexOf("password") < 0) { + return false; + } + + return !(this.passwordFieldExcludeListString.indexOf(cleanedValue) > -1); + } + + private fieldHasDisqualifyingAttributeValue(field: AutofillField): boolean { + const checkedAttributeValues = [field.htmlID, field.htmlName, field.placeholder]; + + for (let i = 0; i < checkedAttributeValues.length; i++) { + const checkedAttributeValue = checkedAttributeValues[i]; + const cleanedValue = checkedAttributeValue?.toLowerCase().replace(/[\s_-]/g, ""); + + if (cleanedValue && this.fieldIgnoreListString.indexOf(cleanedValue) > -1) { + return true; + } + } + + return false; + } + + private isExcludedFieldType(field: AutofillField, excludedTypes: Set): boolean { + if (excludedTypes.has(field.type)) { + return true; + } + + return this.isSearchField(field); + } + + private isSearchField(field: AutofillField): boolean { + const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder]; + for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) { + if (!matchFieldAttributeValues[attrIndex]) { + continue; + } + + // Separate camel case words and case them to lower case values + const camelCaseSeparatedFieldAttribute = matchFieldAttributeValues[attrIndex] + .replace(/([a-z])([A-Z])/g, "$1 $2") + .toLowerCase(); + // Split the attribute by non-alphabetical characters to get the keywords + const attributeKeywords = camelCaseSeparatedFieldAttribute.split(/[^a-z]/gi); + + for (let keywordIndex = 0; keywordIndex < attributeKeywords.length; keywordIndex++) { + if (this.searchFieldNamesSet.has(attributeKeywords[keywordIndex])) { + return true; + } + } + } + + return false; + } +} diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index 7c49a3d9881..b1ee551ed2d 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -4,29 +4,29 @@ import AutofillPageDetails from "../models/autofill-page-details"; import { ElementWithOpId, FillableFormFieldElement, - FormFieldElement, FormElementWithAttribute, + FormFieldElement, } from "../types"; import { elementIsDescriptionDetailsElement, elementIsDescriptionTermElement, elementIsFillableFormField, elementIsFormElement, + elementIsInputElement, elementIsLabelElement, elementIsSelectElement, elementIsSpanElement, - nodeIsFormElement, - nodeIsElement, - elementIsInputElement, elementIsTextAreaElement, + nodeIsElement, + nodeIsFormElement, } from "../utils"; import { AutofillOverlayContentService } from "./abstractions/autofill-overlay-content.service"; import { - UpdateAutofillDataAttributeParams, AutofillFieldElements, AutofillFormElements, CollectAutofillContentService as CollectAutofillContentServiceInterface, + UpdateAutofillDataAttributeParams, } from "./abstractions/collect-autofill-content.service"; import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service"; @@ -35,8 +35,8 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte private readonly autofillOverlayContentService: AutofillOverlayContentService; private noFieldsFound = false; private domRecentlyMutated = true; - private autofillFormElements: AutofillFormElements = new Map(); - private autofillFieldElements: AutofillFieldElements = new Map(); + autofillFormElements: AutofillFormElements = new Map(); + autofillFieldElements: AutofillFieldElements = new Map(); private currentLocationHref = ""; private intersectionObserver: IntersectionObserver; private elementInitializingIntersectionObserver: Set = new Set(); @@ -454,11 +454,11 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @private */ private getAutoCompleteAttribute(element: ElementWithOpId): string { - const autoCompleteType = + return ( this.getPropertyOrAttribute(element, "x-autocompletetype") || this.getPropertyOrAttribute(element, "autocompletetype") || - this.getPropertyOrAttribute(element, "autocomplete"); - return autoCompleteType !== "off" ? autoCompleteType : null; + this.getPropertyOrAttribute(element, "autocomplete") + ); } /** diff --git a/libs/common/src/platform/state/implementations/inline-derived-state.ts b/libs/common/src/platform/state/implementations/inline-derived-state.ts new file mode 100644 index 00000000000..64165ea4bbd --- /dev/null +++ b/libs/common/src/platform/state/implementations/inline-derived-state.ts @@ -0,0 +1,38 @@ +import { Observable, concatMap } from "rxjs"; + +import { DerivedStateDependencies } from "../../../types/state"; +import { DeriveDefinition } from "../derive-definition"; +import { DerivedState } from "../derived-state"; +import { DerivedStateProvider } from "../derived-state.provider"; + +export class InlineDerivedStateProvider implements DerivedStateProvider { + get( + parentState$: Observable, + deriveDefinition: DeriveDefinition, + dependencies: TDeps, + ): DerivedState { + // TODO: Cache? + return new InlineDerivedState(parentState$, deriveDefinition, dependencies); + } +} + +class InlineDerivedState + implements DerivedState +{ + constructor( + parentState$: Observable, + deriveDefinition: DeriveDefinition, + dependencies: TDeps, + ) { + this.state$ = parentState$.pipe( + concatMap(async (value) => await deriveDefinition.derive(value, dependencies)), + ); + } + + state$: Observable; + + forceValue(value: TTo): Promise { + // No need to force anything, we don't keep a cache + return Promise.resolve(value); + } +}