mirror of
https://github.com/bitwarden/browser
synced 2026-01-30 08:13:44 +00:00
[PM-28079] Add attributes to filter for the mutationObserver (#17832)
* [PM-28079] Add attributes to filter for the mutationObserver * Update attributes based on Claude suggestions * Updated remaining attributes * Adjust placeholder check in `updateAutofillFieldElementData` * Update ordering of constants and add comment * Remove `tagName` and `value` from mutation logic * Add new autocomplete and aria attributes to `updateActions` * Fix autocomplete handlers * Fix broken test for `updateAttributes` * Order attributes for readability in `updateActions` * Fix tests --------- Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com>
This commit is contained in:
@@ -158,7 +158,7 @@ describe("CollectAutofillContentService", () => {
|
||||
type: "text",
|
||||
value: "",
|
||||
checked: false,
|
||||
autoCompleteType: "",
|
||||
autoCompleteType: null,
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
selectInfo: null,
|
||||
@@ -346,7 +346,7 @@ describe("CollectAutofillContentService", () => {
|
||||
type: "text",
|
||||
value: "",
|
||||
checked: false,
|
||||
autoCompleteType: "",
|
||||
autoCompleteType: null,
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
selectInfo: null,
|
||||
@@ -379,7 +379,7 @@ describe("CollectAutofillContentService", () => {
|
||||
type: "password",
|
||||
value: "",
|
||||
checked: false,
|
||||
autoCompleteType: "",
|
||||
autoCompleteType: null,
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
selectInfo: null,
|
||||
@@ -588,7 +588,7 @@ describe("CollectAutofillContentService", () => {
|
||||
"aria-disabled": false,
|
||||
"aria-haspopup": false,
|
||||
"aria-hidden": false,
|
||||
autoCompleteType: "",
|
||||
autoCompleteType: null,
|
||||
checked: false,
|
||||
"data-stripe": null,
|
||||
disabled: false,
|
||||
@@ -621,7 +621,7 @@ describe("CollectAutofillContentService", () => {
|
||||
"aria-disabled": false,
|
||||
"aria-haspopup": false,
|
||||
"aria-hidden": false,
|
||||
autoCompleteType: "",
|
||||
autoCompleteType: null,
|
||||
checked: false,
|
||||
"data-stripe": null,
|
||||
disabled: false,
|
||||
@@ -2507,9 +2507,7 @@ describe("CollectAutofillContentService", () => {
|
||||
"class",
|
||||
"tabindex",
|
||||
"title",
|
||||
"value",
|
||||
"rel",
|
||||
"tagname",
|
||||
"checked",
|
||||
"disabled",
|
||||
"readonly",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { AUTOFILL_ATTRIBUTES } from "@bitwarden/common/autofill/constants";
|
||||
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import AutofillForm from "../models/autofill-form";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
@@ -242,10 +244,10 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
this._autofillFormElements.set(formElement, {
|
||||
opid: formElement.opid,
|
||||
htmlAction: this.getFormActionAttribute(formElement),
|
||||
htmlName: this.getPropertyOrAttribute(formElement, "name"),
|
||||
htmlClass: this.getPropertyOrAttribute(formElement, "class"),
|
||||
htmlID: this.getPropertyOrAttribute(formElement, "id"),
|
||||
htmlMethod: this.getPropertyOrAttribute(formElement, "method"),
|
||||
htmlName: this.getPropertyOrAttribute(formElement, AUTOFILL_ATTRIBUTES.NAME),
|
||||
htmlClass: this.getPropertyOrAttribute(formElement, AUTOFILL_ATTRIBUTES.CLASS),
|
||||
htmlID: this.getPropertyOrAttribute(formElement, AUTOFILL_ATTRIBUTES.ID),
|
||||
htmlMethod: this.getPropertyOrAttribute(formElement, AUTOFILL_ATTRIBUTES.METHOD),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -260,7 +262,10 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
* @private
|
||||
*/
|
||||
private getFormActionAttribute(element: ElementWithOpId<HTMLFormElement>): string {
|
||||
return new URL(this.getPropertyOrAttribute(element, "action"), globalThis.location.href).href;
|
||||
return new URL(
|
||||
this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.ACTION),
|
||||
globalThis.location.href,
|
||||
).href;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -335,7 +340,10 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
return priorityFormFields;
|
||||
}
|
||||
|
||||
const fieldType = this.getPropertyOrAttribute(element, "type")?.toLowerCase();
|
||||
const fieldType = this.getPropertyOrAttribute(
|
||||
element,
|
||||
AUTOFILL_ATTRIBUTES.TYPE,
|
||||
)?.toLowerCase();
|
||||
if (unimportantFieldTypesSet.has(fieldType)) {
|
||||
unimportantFormFields.push(element);
|
||||
continue;
|
||||
@@ -384,11 +392,11 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
elementNumber: index,
|
||||
maxLength: this.getAutofillFieldMaxLength(element),
|
||||
viewable: await this.domElementVisibilityService.isElementViewable(element),
|
||||
htmlID: this.getPropertyOrAttribute(element, "id"),
|
||||
htmlName: this.getPropertyOrAttribute(element, "name"),
|
||||
htmlClass: this.getPropertyOrAttribute(element, "class"),
|
||||
tabindex: this.getPropertyOrAttribute(element, "tabindex"),
|
||||
title: this.getPropertyOrAttribute(element, "title"),
|
||||
htmlID: this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.ID),
|
||||
htmlName: this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.NAME),
|
||||
htmlClass: this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.CLASS),
|
||||
tabindex: this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.TABINDEX),
|
||||
title: this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.TITLE),
|
||||
tagName: this.getAttributeLowerCase(element, "tagName"),
|
||||
dataSetValues: this.getDataSetValues(element),
|
||||
};
|
||||
@@ -404,16 +412,16 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
}
|
||||
|
||||
let autofillFieldLabels = {};
|
||||
const elementType = this.getAttributeLowerCase(element, "type");
|
||||
const elementType = this.getAttributeLowerCase(element, AUTOFILL_ATTRIBUTES.TYPE);
|
||||
if (elementType !== "hidden") {
|
||||
autofillFieldLabels = {
|
||||
"label-tag": this.createAutofillFieldLabelTag(element as FillableFormFieldElement),
|
||||
"label-data": this.getPropertyOrAttribute(element, "data-label"),
|
||||
"label-aria": this.getPropertyOrAttribute(element, "aria-label"),
|
||||
"label-data": this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.DATA_LABEL),
|
||||
"label-aria": this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.ARIA_LABEL),
|
||||
"label-top": this.createAutofillFieldTopLabel(element),
|
||||
"label-right": this.createAutofillFieldRightLabel(element),
|
||||
"label-left": this.createAutofillFieldLeftLabel(element),
|
||||
placeholder: this.getPropertyOrAttribute(element, "placeholder"),
|
||||
placeholder: this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.PLACEHOLDER),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -421,21 +429,21 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
const autofillField = {
|
||||
...autofillFieldBase,
|
||||
...autofillFieldLabels,
|
||||
rel: this.getPropertyOrAttribute(element, "rel"),
|
||||
rel: this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.REL),
|
||||
type: elementType,
|
||||
value: this.getElementValue(element),
|
||||
checked: this.getAttributeBoolean(element, "checked"),
|
||||
checked: this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.CHECKED),
|
||||
autoCompleteType: this.getAutoCompleteAttribute(element),
|
||||
disabled: this.getAttributeBoolean(element, "disabled"),
|
||||
readonly: this.getAttributeBoolean(element, "readonly"),
|
||||
disabled: this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.DISABLED),
|
||||
readonly: this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.READONLY),
|
||||
selectInfo: elementIsSelectElement(element)
|
||||
? this.getSelectElementOptions(element as HTMLSelectElement)
|
||||
: null,
|
||||
form: fieldFormElement ? this.getPropertyOrAttribute(fieldFormElement, "opid") : null,
|
||||
"aria-hidden": this.getAttributeBoolean(element, "aria-hidden", true),
|
||||
"aria-disabled": this.getAttributeBoolean(element, "aria-disabled", true),
|
||||
"aria-haspopup": this.getAttributeBoolean(element, "aria-haspopup", true),
|
||||
"data-stripe": this.getPropertyOrAttribute(element, "data-stripe"),
|
||||
"aria-hidden": this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.ARIA_HIDDEN, true),
|
||||
"aria-disabled": this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.ARIA_DISABLED, true),
|
||||
"aria-haspopup": this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.ARIA_HASPOPUP, true),
|
||||
"data-stripe": this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.DATA_STRIPE),
|
||||
};
|
||||
|
||||
this.cacheAutofillFieldElement(index, element, autofillField);
|
||||
@@ -467,9 +475,9 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
*/
|
||||
private getAutoCompleteAttribute(element: ElementWithOpId<FormFieldElement>): string {
|
||||
return (
|
||||
this.getPropertyOrAttribute(element, "x-autocompletetype") ||
|
||||
this.getPropertyOrAttribute(element, "autocompletetype") ||
|
||||
this.getPropertyOrAttribute(element, "autocomplete")
|
||||
this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.AUTOCOMPLETE) ||
|
||||
this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.X_AUTOCOMPLETE_TYPE) ||
|
||||
this.getPropertyOrAttribute(element, AUTOFILL_ATTRIBUTES.AUTOCOMPLETE_TYPE)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -957,6 +965,8 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
this.mutationObserver = new MutationObserver(this.handleMutationObserverMutation);
|
||||
this.mutationObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
/** Mutations to node attributes NOT on this list will not be observed! */
|
||||
attributeFilter: Object.values(AUTOFILL_ATTRIBUTES),
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
@@ -1321,6 +1331,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
action: () => (dataTarget.htmlAction = this.getFormActionAttribute(element)),
|
||||
name: () => updateAttribute("htmlName"),
|
||||
id: () => updateAttribute("htmlID"),
|
||||
class: () => updateAttribute("htmlClass"),
|
||||
method: () => updateAttribute("htmlMethod"),
|
||||
};
|
||||
|
||||
@@ -1350,29 +1361,49 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
this.updateAutofillDataAttribute({ element, attributeName, dataTarget, dataTargetKey });
|
||||
};
|
||||
const updateActions: Record<string, CallableFunction> = {
|
||||
maxlength: () => (dataTarget.maxLength = this.getAutofillFieldMaxLength(element)),
|
||||
id: () => updateAttribute("htmlID"),
|
||||
name: () => updateAttribute("htmlName"),
|
||||
class: () => updateAttribute("htmlClass"),
|
||||
tabindex: () => updateAttribute("tabindex"),
|
||||
title: () => updateAttribute("tabindex"),
|
||||
rel: () => updateAttribute("rel"),
|
||||
tagname: () => (dataTarget.tagName = this.getAttributeLowerCase(element, "tagName")),
|
||||
type: () => (dataTarget.type = this.getAttributeLowerCase(element, "type")),
|
||||
value: () => (dataTarget.value = this.getElementValue(element)),
|
||||
checked: () => (dataTarget.checked = this.getAttributeBoolean(element, "checked")),
|
||||
disabled: () => (dataTarget.disabled = this.getAttributeBoolean(element, "disabled")),
|
||||
readonly: () => (dataTarget.readonly = this.getAttributeBoolean(element, "readonly")),
|
||||
autocomplete: () => (dataTarget.autoCompleteType = this.getAutoCompleteAttribute(element)),
|
||||
"data-label": () => updateAttribute("label-data"),
|
||||
"aria-describedby": () => updateAttribute(AUTOFILL_ATTRIBUTES.ARIA_DESCRIBEDBY),
|
||||
"aria-label": () => updateAttribute("label-aria"),
|
||||
"aria-labelledby": () => updateAttribute(AUTOFILL_ATTRIBUTES.ARIA_LABELLEDBY),
|
||||
"aria-hidden": () =>
|
||||
(dataTarget["aria-hidden"] = this.getAttributeBoolean(element, "aria-hidden", true)),
|
||||
(dataTarget["aria-hidden"] = this.getAttributeBoolean(
|
||||
element,
|
||||
AUTOFILL_ATTRIBUTES.ARIA_HIDDEN,
|
||||
true,
|
||||
)),
|
||||
"aria-disabled": () =>
|
||||
(dataTarget["aria-disabled"] = this.getAttributeBoolean(element, "aria-disabled", true)),
|
||||
(dataTarget["aria-disabled"] = this.getAttributeBoolean(
|
||||
element,
|
||||
AUTOFILL_ATTRIBUTES.ARIA_DISABLED,
|
||||
true,
|
||||
)),
|
||||
"aria-haspopup": () =>
|
||||
(dataTarget["aria-haspopup"] = this.getAttributeBoolean(element, "aria-haspopup", true)),
|
||||
"data-stripe": () => updateAttribute("data-stripe"),
|
||||
(dataTarget["aria-haspopup"] = this.getAttributeBoolean(
|
||||
element,
|
||||
AUTOFILL_ATTRIBUTES.ARIA_HASPOPUP,
|
||||
true,
|
||||
)),
|
||||
autocomplete: () => (dataTarget.autoCompleteType = this.getAutoCompleteAttribute(element)),
|
||||
autocompletetype: () =>
|
||||
(dataTarget.autoCompleteType = this.getAutoCompleteAttribute(element)),
|
||||
"x-autocompletetype": () =>
|
||||
(dataTarget.autoCompleteType = this.getAutoCompleteAttribute(element)),
|
||||
class: () => updateAttribute("htmlClass"),
|
||||
checked: () =>
|
||||
(dataTarget.checked = this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.CHECKED)),
|
||||
"data-label": () => updateAttribute("label-data"),
|
||||
"data-stripe": () => updateAttribute(AUTOFILL_ATTRIBUTES.DATA_STRIPE),
|
||||
disabled: () =>
|
||||
(dataTarget.disabled = this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.DISABLED)),
|
||||
id: () => updateAttribute("htmlID"),
|
||||
maxlength: () => (dataTarget.maxLength = this.getAutofillFieldMaxLength(element)),
|
||||
name: () => updateAttribute("htmlName"),
|
||||
placeholder: () => updateAttribute(AUTOFILL_ATTRIBUTES.PLACEHOLDER),
|
||||
readonly: () =>
|
||||
(dataTarget.readonly = this.getAttributeBoolean(element, AUTOFILL_ATTRIBUTES.READONLY)),
|
||||
rel: () => updateAttribute(AUTOFILL_ATTRIBUTES.REL),
|
||||
tabindex: () => updateAttribute(AUTOFILL_ATTRIBUTES.TABINDEX),
|
||||
title: () => updateAttribute(AUTOFILL_ATTRIBUTES.TITLE),
|
||||
type: () => (dataTarget.type = this.getAttributeLowerCase(element, AUTOFILL_ATTRIBUTES.TYPE)),
|
||||
};
|
||||
|
||||
if (!updateActions[attributeName]) {
|
||||
|
||||
@@ -28,6 +28,41 @@ export const EVENTS = {
|
||||
SUBMIT: "submit",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* HTML attributes observed by the MutationObserver for autofill form/field tracking.
|
||||
* If you need to observe a new attribute, add it here.
|
||||
*/
|
||||
export const AUTOFILL_ATTRIBUTES = {
|
||||
ACTION: "action",
|
||||
ARIA_DESCRIBEDBY: "aria-describedby",
|
||||
ARIA_DISABLED: "aria-disabled",
|
||||
ARIA_HASPOPUP: "aria-haspopup",
|
||||
ARIA_HIDDEN: "aria-hidden",
|
||||
ARIA_LABEL: "aria-label",
|
||||
ARIA_LABELLEDBY: "aria-labelledby",
|
||||
AUTOCOMPLETE: "autocomplete",
|
||||
AUTOCOMPLETE_TYPE: "autocompletetype",
|
||||
X_AUTOCOMPLETE_TYPE: "x-autocompletetype",
|
||||
CHECKED: "checked",
|
||||
CLASS: "class",
|
||||
DATA_LABEL: "data-label",
|
||||
DATA_STRIPE: "data-stripe",
|
||||
DISABLED: "disabled",
|
||||
ID: "id",
|
||||
MAXLENGTH: "maxlength",
|
||||
METHOD: "method",
|
||||
NAME: "name",
|
||||
PLACEHOLDER: "placeholder",
|
||||
POPOVER: "popover",
|
||||
POPOVERTARGET: "popovertarget",
|
||||
POPOVERTARGETACTION: "popovertargetaction",
|
||||
READONLY: "readonly",
|
||||
REL: "rel",
|
||||
TABINDEX: "tabindex",
|
||||
TITLE: "title",
|
||||
TYPE: "type",
|
||||
} as const;
|
||||
|
||||
export const ClearClipboardDelay = {
|
||||
Never: null as null,
|
||||
TenSeconds: 10,
|
||||
|
||||
Reference in New Issue
Block a user