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:
@@ -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 {
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user