1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-14 15:33:55 +00:00

use URL targeting rules for field autofill

This commit is contained in:
Jonathan Prusik
2025-10-13 18:55:16 -04:00
parent 5886f9f94f
commit 6809519184
4 changed files with 186 additions and 7 deletions

View File

@@ -1,9 +1,20 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { AutofillFieldQualifierType } from "@bitwarden/common/autofill/types";
// String values affect code flow in autofill.ts and must not be changed
export type FillScriptActions = "click_on_opid" | "focus_by_opid" | "fill_by_opid";
export type FillScriptActions =
| "click_on_opid"
| "focus_by_opid"
| "fill_by_opid"
| "fill_by_targeted_field_type"
| "click_on_targeted_field_type"
| "focus_by_targeted_field_type";
export type FillScript = [action: FillScriptActions, opid: string, value?: string];
export type FillScript = [
action: FillScriptActions,
actionTarget: AutofillFieldQualifierType | string,
value?: string,
];
export type AutofillScriptProperties = {
delay_between_operations?: number;
@@ -13,6 +24,15 @@ export type AutofillInsertActions = {
fill_by_opid: ({ opid, value }: { opid: string; value: string }) => void;
click_on_opid: ({ opid }: { opid: string }) => void;
focus_by_opid: ({ opid }: { opid: string }) => void;
fill_by_targeted_field_type: ({
fieldType,
value,
}: {
fieldType: AutofillFieldQualifierType;
value: string;
}) => void;
click_on_targeted_field_type: ({ fieldType }: { fieldType: AutofillFieldQualifierType }) => void;
focus_by_targeted_field_type: ({ fieldType }: { fieldType: AutofillFieldQualifierType }) => void;
};
export default class AutofillScript {

View File

@@ -2,6 +2,7 @@
// @ts-strict-ignore
import { Observable } from "rxjs";
import { AutofillTargetingRules } from "@bitwarden/common/autofill/types";
import { UriMatchStrategySetting } from "@bitwarden/common/models/domain/domain-service";
import { CommandDefinition } from "@bitwarden/common/platform/messaging";
import { CipherType } from "@bitwarden/common/vault/enums";
@@ -49,6 +50,7 @@ export interface GenerateFillScriptOptions {
cipher: CipherView;
tabUrl: string;
defaultUriMatch: UriMatchStrategySetting;
pageTargetingRules?: AutofillTargetingRules;
}
export type CollectPageDetailsResponseMessage = {
@@ -73,6 +75,7 @@ export abstract class AutofillService {
triggeringOnPageLoad?: boolean,
) => Promise<void>;
getFormsWithPasswordFields: (pageDetails: AutofillPageDetails) => FormData[];
getPageTagetingRules: (url: chrome.tabs.Tab["url"]) => Promise<AutofillTargetingRules>;
doAutoFill: (options: AutoFillOptions) => Promise<string | null>;
doAutoFillOnTab: (
pageDetails: PageDetail[],

View File

@@ -19,13 +19,18 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import {
AutofillFieldQualifier,
AutofillOverlayVisibility,
CardExpiryDateDelimiters,
} from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
import {
AutofillFieldQualifierType,
AutofillTargetingRules,
InlineMenuVisibilitySetting,
} from "@bitwarden/common/autofill/types";
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
@@ -403,6 +408,21 @@ export default class AutofillService implements AutofillServiceInterface {
return await firstValueFrom(this.domainSettingsService.resolvedDefaultUriMatchStrategy$);
}
/**
* Gets the default URI match strategy setting from the domain settings service.
*/
async getPageTagetingRules(url: chrome.tabs.Tab["url"]): Promise<AutofillTargetingRules> {
if (url) {
const targetingRules = await firstValueFrom(
this.domainSettingsService.getUrlAutofillTargetingRules$(url),
);
return targetingRules;
}
return null;
}
/**
* Autofill a given tab with a given login item
* @param {AutoFillOptions} options Instructions about the autofill operation, including tab and login item
@@ -410,6 +430,8 @@ export default class AutofillService implements AutofillServiceInterface {
*/
async doAutoFill(options: AutoFillOptions): Promise<string | null> {
const tab = options.tab;
const pageTargetingRules = await this.getPageTagetingRules(tab.url);
if (!tab || !options.cipher || !options.pageDetails || !options.pageDetails.length) {
throw new Error("Nothing to autofill.");
}
@@ -451,6 +473,7 @@ export default class AutofillService implements AutofillServiceInterface {
cipher: options.cipher,
tabUrl: tab.url,
defaultUriMatch: defaultUriMatch,
pageTargetingRules,
});
if (!fillScript || !fillScript.script || !fillScript.script.length) {
@@ -740,8 +763,12 @@ export default class AutofillService implements AutofillServiceInterface {
let fillScript = new AutofillScript();
const filledFields: { [id: string]: AutofillField } = {};
const fields = options.cipher.fields;
const pageTargetedFields = Object.keys(
options.pageTargetingRules,
) as AutofillFieldQualifierType[];
const pageHasTargetingRules = !!pageTargetedFields.length;
if (fields && fields.length) {
if (fields && fields.length && !pageHasTargetingRules) {
const fieldNames: string[] = [];
fields.forEach((f) => {
@@ -787,6 +814,17 @@ export default class AutofillService implements AutofillServiceInterface {
switch (options.cipher.type) {
case CipherType.Login:
if (pageHasTargetingRules) {
for (const fieldType of pageTargetedFields) {
const fieldTypeValue = this.findCipherPropertyValueFieldType(options.cipher, fieldType);
if (fieldTypeValue) {
AutofillService.fillByTargetingRule(fillScript, fieldType, fieldTypeValue);
}
}
break;
}
fillScript = await this.generateLoginFillScript(
fillScript,
pageDetails,
@@ -2421,6 +2459,29 @@ export default class AutofillService implements AutofillServiceInterface {
return totpField;
}
private findCipherPropertyValueFieldType(
cipher: CipherView,
fieldType: AutofillFieldQualifierType | "totp",
) {
let cipherPropertyValue = null;
switch (fieldType) {
case AutofillFieldQualifier.username:
cipherPropertyValue = cipher.login?.username || null;
break;
case AutofillFieldQualifier.password:
cipherPropertyValue = cipher.login?.password || null;
break;
case "totp":
cipherPropertyValue = cipher.login?.totp || null;
break;
default:
break;
}
return cipherPropertyValue;
}
/**
* Accepts a field and returns the index of the first matching property
* present in a list of attribute names.
@@ -2695,6 +2756,16 @@ export default class AutofillService implements AutofillServiceInterface {
fillScript.script.push(["fill_by_opid", field.opid, value]);
}
static fillByTargetingRule(
fillScript: AutofillScript,
fieldType: AutofillFieldQualifierType,
value: string,
): void {
fillScript.script.push(["click_on_targeted_field_type", fieldType]);
fillScript.script.push(["focus_by_targeted_field_type", fieldType]);
fillScript.script.push(["fill_by_targeted_field_type", fieldType, value]);
}
/**
* Identifies if the field is a custom field, a custom
* field is defined as a field that is a `span` element.

View File

@@ -1,6 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { EVENTS, TYPE_CHECK } from "@bitwarden/common/autofill/constants";
import { AutofillFieldQualifierType } from "@bitwarden/common/autofill/types";
import AutofillScript, { AutofillInsertActions, FillScript } from "../models/autofill-script";
import { FormFieldElement } from "../types";
@@ -21,6 +22,12 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
fill_by_opid: ({ opid, value }) => this.handleFillFieldByOpidAction(opid, value),
click_on_opid: ({ opid }) => this.handleClickOnFieldByOpidAction(opid),
focus_by_opid: ({ opid }) => this.handleFocusOnFieldByOpidAction(opid),
fill_by_targeted_field_type: ({ fieldType, value }) =>
this.handleFillFieldByTargetedFieldTypeAction(fieldType, value),
click_on_targeted_field_type: ({ fieldType }) =>
this.handleClickOnFieldByTargetedFieldTypeAction(fieldType),
focus_by_targeted_field_type: ({ fieldType }) =>
this.handleFocusOnFieldByTargetedFieldTypeAction(fieldType),
};
/**
@@ -123,17 +130,40 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
* @private
*/
private runFillScriptAction = (
[action, opid, value]: FillScript,
[action, actionTarget, value]: FillScript,
actionIndex: number,
): Promise<void> => {
if (!opid || !this.autofillInsertActions[action]) {
if (!actionTarget || !this.autofillInsertActions[action]) {
return;
}
const delayActionsInMilliseconds = 20;
return new Promise((resolve) =>
setTimeout(() => {
this.autofillInsertActions[action]({ opid, value });
switch (action) {
case "fill_by_targeted_field_type":
this.autofillInsertActions[action]({
fieldType: actionTarget as AutofillFieldQualifierType,
value,
});
break;
case "click_on_targeted_field_type":
case "focus_by_targeted_field_type":
this.autofillInsertActions[action]({
fieldType: actionTarget as AutofillFieldQualifierType,
});
break;
case "fill_by_opid":
this.autofillInsertActions[action]({ opid: actionTarget, value });
break;
case "click_on_opid":
case "focus_by_opid":
this.autofillInsertActions[action]({ opid: actionTarget });
break;
default:
break;
}
resolve();
}, delayActionsInMilliseconds * actionIndex),
);
@@ -177,6 +207,61 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
this.simulateUserMouseClickAndFocusEventInteractions(element, true);
}
/**
* Handles finding an element by targeted field type and inserts the passed value into the element.
* @param {AutofillFieldQualifierType} fieldType
* @param {string} value
* @private
*/
private handleFillFieldByTargetedFieldTypeAction(
fieldType: AutofillFieldQualifierType,
value: string,
) {
const targetedElement = this.getElementByTargetedFieldType(fieldType);
if (targetedElement) {
this.insertValueIntoField(targetedElement, value);
}
}
/**
* Handles finding an element by targeted field type and triggering a click event on the element.
* @param {AutofillFieldQualifierType} fieldType
* @private
*/
private handleClickOnFieldByTargetedFieldTypeAction(fieldType: AutofillFieldQualifierType) {
const targetedElement = this.getElementByTargetedFieldType(fieldType);
if (targetedElement) {
this.triggerClickOnElement(targetedElement);
}
}
/**
* Handles finding an element by targeted field type and triggering click and focus events on the element.
* To ensure that we trigger a blur event correctly on a filled field, we first check if the
* element is already focused. If it is, we blur the element before focusing on it again.
*
* @param {AutofillFieldQualifierType} fieldType
*/
private handleFocusOnFieldByTargetedFieldTypeAction(fieldType: AutofillFieldQualifierType) {
const targetedElement = this.getElementByTargetedFieldType(fieldType);
if (targetedElement) {
if (document.activeElement === targetedElement) {
targetedElement.blur();
}
this.simulateUserMouseClickAndFocusEventInteractions(targetedElement, true);
}
}
private getElementByTargetedFieldType(fieldType: AutofillFieldQualifierType) {
const targetedElements = this.collectAutofillContentService.getTargetedFields();
return (targetedElements[fieldType] as HTMLElement) || null;
}
/**
* Identifies the type of element passed and inserts the value into the element.
* Will trigger simulated events on the element to ensure that the element is