Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/browser/src/autofill/models/autofill-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,6 @@ export default class AutofillField {
*/
fieldRect?: FieldRect;
}

/** `readonly` / `disabled` from collected field data; a full {@link AutofillField} is assignable. */
export type AutofillFieldReadonlyDisabledState = Pick<AutofillField, "readonly" | "disabled">;
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
elementIsFillableFormField,
elementIsSelectElement,
getAttributeBoolean,
isReadonlyOrDisabledFormFieldElement,
nodeIsAnchorElement,
nodeIsButtonElement,
nodeIsTypeSubmitElement,
Expand Down Expand Up @@ -216,7 +217,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
return;
}

if (this.isReadonlyOrDisabledElement(formFieldElement, autofillFieldData)) {
if (isReadonlyOrDisabledFormFieldElement(formFieldElement, autofillFieldData)) {
return;
}

Expand Down Expand Up @@ -764,7 +765,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
*/
private async focusInlineMenuList() {
if (this.mostRecentlyFocusedField && !(await this.isInlineMenuListVisible())) {
if (this.isReadonlyOrDisabledElement(this.mostRecentlyFocusedField)) {
if (isReadonlyOrDisabledFormFieldElement(this.mostRecentlyFocusedField)) {
return;
}
this.clearFocusInlineMenuListTimeout();
Expand Down Expand Up @@ -803,7 +804,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
if (!elementIsFillableFormField(formFieldElement)) {
return;
}
if (this.isReadonlyOrDisabledElement(formFieldElement)) {
if (isReadonlyOrDisabledFormFieldElement(formFieldElement)) {
return;
}

Expand Down Expand Up @@ -928,7 +929,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
* @param formFieldElement - The form field element that triggered the click event.
*/
private async triggerFormFieldClickedAction(formFieldElement: ElementWithOpId<FormFieldElement>) {
if (this.isReadonlyOrDisabledElement(formFieldElement)) {
if (isReadonlyOrDisabledFormFieldElement(formFieldElement)) {
return;
}

Expand Down Expand Up @@ -962,7 +963,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
if (await this.isFieldCurrentlyFilling()) {
return;
}
if (this.isReadonlyOrDisabledElement(formFieldElement)) {
if (isReadonlyOrDisabledFormFieldElement(formFieldElement)) {
return;
}

Expand Down Expand Up @@ -1474,25 +1475,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
return documentRoot?.activeElement;
}

/**
* Checks if a form field element is currently readonly or disabled.
*
* @param formFieldElement - The form field element to evaluate.
* @param autofillFieldData - Optional cached autofill metadata for readonly or disabled state.
*/
private isReadonlyOrDisabledElement(
formFieldElement: ElementWithOpId<FormFieldElement>,
autofillFieldData?: AutofillField,
): boolean {
return (
getAttributeBoolean(formFieldElement, "disabled") ||
Boolean((formFieldElement as HTMLInputElement | HTMLTextAreaElement).readOnly) ||
getAttributeBoolean(formFieldElement, "aria-readonly", true) ||
autofillFieldData?.readonly === true ||
autofillFieldData?.disabled === true
);
}

/**
* Queries all iframe elements within the document and returns the
* sub frame offsets for each iframe element.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,18 @@ describe("InsertAutofillContentService", () => {
).not.toHaveBeenCalled();
expect(element.value).toBe(value);
});

it("does not insert when aria-readonly is set", () => {
document.body.innerHTML = `<input type="text" id="username" aria-readonly="true" />`;
const element = document.getElementById("username") as HTMLInputElement;
jest.spyOn(insertAutofillContentService as any, "handleInsertValueAndTriggerSimulatedEvents");

insertAutofillContentService["insertValueIntoField"](element, "new-value");

expect(
insertAutofillContentService["handleInsertValueAndTriggerSimulatedEvents"],
).not.toHaveBeenCalled();
});
});

describe("handleInsertValueAndTriggerSimulatedEvents", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import {
currentlyInSandboxedIframe,
elementIsFillableFormField,
elementIsInputElement,
elementIsSelectElement,
elementIsTextAreaElement,
isReadonlyOrDisabledFormFieldElement,
} from "../utils";

import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service";
Expand Down Expand Up @@ -204,18 +203,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
return;
}

const elementCanBeReadonly =
elementIsInputElement(element) || elementIsTextAreaElement(element);
const elementCanBeFilled = elementCanBeReadonly || elementIsSelectElement(element);
const elementValue = (element as HTMLInputElement)?.value || element?.innerText || "";

const elementAlreadyHasTheValue = !!(elementValue?.length && elementValue === value);

if (
elementAlreadyHasTheValue ||
(elementCanBeReadonly && element.readOnly) ||
(elementCanBeFilled && element.disabled)
) {
if (elementAlreadyHasTheValue || isReadonlyOrDisabledFormFieldElement(element)) {
return;
}

Expand Down
33 changes: 33 additions & 0 deletions apps/browser/src/autofill/utils/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
buildSvgDomElement,
debounce,
generateRandomCustomElementName,
isReadonlyOrDisabledFormFieldElement,
sendExtensionMessage,
setElementStyles,
setupAutofillInitDisconnectAction,
Expand Down Expand Up @@ -244,3 +245,35 @@ describe("debounce", () => {
expect(debouncedFunction).toHaveBeenCalledTimes(1);
});
});

describe("isReadonlyOrDisabledFormFieldElement", () => {
it("returns false for an enabled, editable text input", () => {
document.body.innerHTML = `<input type="text" id="field" />`;
expect(
isReadonlyOrDisabledFormFieldElement(document.getElementById("field") as HTMLInputElement),
).toBe(false);
});

it("returns true when DOM or cached flags indicate the field is not writable", () => {
const expectTrue = (html: string, meta?: { readonly?: boolean; disabled?: boolean }) => {
document.body.innerHTML = html;
expect(
isReadonlyOrDisabledFormFieldElement(
document.getElementById("field") as HTMLInputElement,
meta,
),
).toBe(true);
};

expectTrue(`<input type="text" id="field" disabled />`);
expectTrue(`<input type="text" id="field" readonly />`);
expectTrue(`<input type="text" id="field" aria-readonly="true" />`);
expectTrue(`<input type="text" id="field" />`, { readonly: true });
expectTrue(`<input type="text" id="field" />`, { disabled: true });

document.body.innerHTML = `<textarea id="field"></textarea>`;
const textarea = document.getElementById("field") as HTMLTextAreaElement;
textarea.readOnly = true;
expect(isReadonlyOrDisabledFormFieldElement(textarea)).toBe(true);
});
});
26 changes: 26 additions & 0 deletions apps/browser/src/autofill/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { AUTOFILL_ATTRIBUTES } from "@bitwarden/common/autofill/constants";

import { FieldRect } from "../background/abstractions/overlay.background";
import { AutofillPort } from "../enums/autofill-port.enum";
import type { AutofillFieldReadonlyDisabledState } from "../models/autofill-field";
import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types";

/**
Expand Down Expand Up @@ -351,6 +354,29 @@ export function getAttributeBoolean(
return Boolean(getPropertyOrAttribute(element, attributeName));
}

/**
* Checks if a form field element is currently readonly or disabled.
*
* @param formFieldElement - The form field element to evaluate.
* @param autofillFieldData - Optional cached autofill metadata for readonly or disabled state.
*/
export function isReadonlyOrDisabledFormFieldElement(
formFieldElement: FormFieldElement,
autofillFieldData?: AutofillFieldReadonlyDisabledState,
): boolean {
const readOnlyByProperty =
(elementIsInputElement(formFieldElement) || elementIsTextAreaElement(formFieldElement)) &&
formFieldElement.readOnly;

return (
getAttributeBoolean(formFieldElement, AUTOFILL_ATTRIBUTES.DISABLED) ||
readOnlyByProperty ||
getAttributeBoolean(formFieldElement, "aria-readonly", true) ||
autofillFieldData?.readonly === true ||
autofillFieldData?.disabled === true
);
}

/**
* Get the value of a property or attribute from a FormFieldElement.
*
Expand Down
Loading