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 3c7ad3a135e..ef6c6518a20 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 @@ -1,3 +1,5 @@ +import { AutofillFieldQualifierType } from "@bitwarden/common/autofill/types"; + import AutofillField from "../../models/autofill-field"; import AutofillForm from "../../models/autofill-form"; import AutofillPageDetails from "../../models/autofill-page-details"; @@ -14,11 +16,13 @@ type UpdateAutofillDataAttributeParams = { dataTargetKey?: string; }; +type TargetedFields = { [type in AutofillFieldQualifierType | "totp"]?: Element }; + interface CollectAutofillContentService { autofillFormElements: AutofillFormElements; getPageDetails(): Promise; getAutofillFieldElementByOpid(opid: string): HTMLElement | null; - getTargetedFields(): {[key: string]: Element} | null; + getTargetedFields(): TargetedFields; destroy(): void; } @@ -27,4 +31,5 @@ export { AutofillFieldElements, UpdateAutofillDataAttributeParams, CollectAutofillContentService, + TargetedFields, }; 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 728b3f85a03..e25f7b0bcc7 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -1,8 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { AutofillFieldQualifierType } from "@bitwarden/common/autofill/types"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { AutofillFieldQualifierType } from "../enums/autofill-field.enums"; import AutofillField from "../models/autofill-field"; import AutofillForm from "../models/autofill-form"; import AutofillPageDetails from "../models/autofill-page-details"; @@ -34,12 +34,11 @@ import { AutofillFormElements, CollectAutofillContentService as CollectAutofillContentServiceInterface, UpdateAutofillDataAttributeParams, + TargetedFields, } from "./abstractions/collect-autofill-content.service"; import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service"; import { DomQueryService } from "./abstractions/dom-query.service"; -type TargetedFields = { [type in AutofillFieldQualifierType]?: Element }; - export class CollectAutofillContentService implements CollectAutofillContentServiceInterface { private readonly sendExtensionMessage = sendExtensionMessage; private readonly getAttributeBoolean = getAttributeBoolean; @@ -52,7 +51,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ /** * A value of `null` indicates the configuration has not been initialized, whereas an empty object indicates no set rules. */ - private readonly pageTargetingRules: null | { [type in AutofillFieldQualifierType]?: string } = {}; + private pageTargetingRules: null | { [type in AutofillFieldQualifierType]?: string } = null; private intersectionObserver: IntersectionObserver; private elementInitializingIntersectionObserver: Set = new Set(); private mutationObserver: MutationObserver; @@ -81,6 +80,10 @@ export class CollectAutofillContentService implements CollectAutofillContentServ inputQuery += `:not([type="${type}"])`; } this.formFieldQueryString = `${inputQuery}, textarea:not([data-bwignore]), select:not([data-bwignore]), span[data-bwautofill]`; + + void sendExtensionMessage("getUrlAutofillTargetingRules").then((targetingRules) => { + this.pageTargetingRules = targetingRules?.result ?? null; + }); } get autofillFormElements(): AutofillFormElements { @@ -99,31 +102,8 @@ export class CollectAutofillContentService implements CollectAutofillContentServ this.setupInitialTopLayerListeners(); const targetedFields = this.getTargetedFields(); - const targetedFieldNames = Object.keys(targetedFields) as AutofillFieldQualifierType[]; - if (targetedFieldNames.length) { - for (const fieldName of targetedFieldNames) { - const fieldNode = targetedFields[fieldName] as ElementWithOpId; - void this.autofillOverlayContentService.setupOverlayListenersOnQualifiedField( - fieldNode, - { - // @TODO needs an opid and added to the list in order for autofill to work - opid: fieldNode.opid || `targeted_field_${fieldName}`, - elementNumber: 0, // Not relevant when using targeting rules - viewable: true, - htmlID: fieldNode.getAttribute('id'), - htmlName: fieldNode.getAttribute('name'), - htmlClass: fieldNode.getAttribute('class'), - tabindex: fieldNode.getAttribute('tabindex'), - title: fieldNode.getAttribute('title'), - inlineMenuFillType: CipherType.Login, - fieldQualifier: fieldName, - }, - ); - } - - return {} as AutofillPageDetails; - } + const targetedFieldNames = Object.keys(targetedFields) as Array; if (!this.mutationObserver) { this.setupMutationObserver(); @@ -149,7 +129,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ const { formElements, formFieldElements } = this.queryAutofillFormAndFieldElements(); const autofillFormsData: Record = this.buildAutofillFormsData(formElements); - const autofillFieldsData: AutofillField[] = ( + let autofillFieldsData: AutofillField[] = ( await this.buildAutofillFieldsData(formFieldElements as FormFieldElement[]) ).filter((field) => !!field); this.sortAutofillFieldElementsMap(); @@ -161,6 +141,35 @@ export class CollectAutofillContentService implements CollectAutofillContentServ this.domRecentlyMutated = false; const pageDetails = this.getFormattedPageDetails(autofillFormsData, autofillFieldsData); + if (targetedFieldNames.length) { + autofillFieldsData = []; + this.noFieldsFound = false; + + for (const fieldName of targetedFieldNames) { + const fieldNode = targetedFields[fieldName] as ElementWithOpId; + const autofillField = { + opid: `targeted_field_${fieldName}`, + elementNumber: 0, // Not relevant when using targeting rules + viewable: true, + htmlID: fieldNode.getAttribute("id"), + htmlName: fieldNode.getAttribute("name"), + htmlClass: fieldNode.getAttribute("class"), + tabindex: fieldNode.getAttribute("tabindex"), + title: fieldNode.getAttribute("title"), + inlineMenuFillType: CipherType.Login, + fieldQualifier: fieldName, + }; + autofillFieldsData = [...autofillFieldsData, autofillField]; + + void this.autofillOverlayContentService.setupOverlayListenersOnQualifiedField( + fieldNode, + autofillField, + ); + } + + return { ...pageDetails, fields: autofillFieldsData } as AutofillPageDetails; + } + this.setupOverlayListeners(pageDetails); return pageDetails; @@ -186,12 +195,12 @@ export class CollectAutofillContentService implements CollectAutofillContentServ [fieldName]: fieldMatches[0], } : foundFields; - }, {}); + }, {} as TargetedFields); return foundTargetedFields; } - return {}; + return {} as TargetedFields; } /** diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 9dc2bff65e5..e364157820e 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -79,6 +79,7 @@ export default class RuntimeBackground { BiometricsCommands.UnlockWithBiometricsForUser, BiometricsCommands.GetBiometricsStatusForUser, BiometricsCommands.CanEnableBiometricUnlock, + "getUrlAutofillTargetingRules", "getUserPremiumStatus", ]; @@ -204,6 +205,15 @@ export default class RuntimeBackground { case BiometricsCommands.CanEnableBiometricUnlock: { return await this.main.biometricsService.canEnableBiometricUnlock(); } + case "getUrlAutofillTargetingRules": { + if (sender.tab?.url) { + const targetingRules = await this.autofillService.getPageTagetingRules(sender.tab.url); + + return targetingRules; + } + + return null; + } case "getUserPremiumStatus": { const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)),