1
0
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:
Jeffrey Holland
2026-01-27 17:28:02 +01:00
committed by GitHub
parent fe1410bed3
commit 00cf24972d
3 changed files with 116 additions and 52 deletions

View File

@@ -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",

View File

@@ -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]) {

View File

@@ -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,