diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 7e1f811247..21baf9a3cd 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -10,7 +10,12 @@ import AutofillField from "../models/autofill-field"; import AutofillOverlayButtonIframe from "../overlay/iframe-content/autofill-overlay-button-iframe"; import AutofillOverlayListIframe from "../overlay/iframe-content/autofill-overlay-list-iframe"; import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types"; -import { generateRandomCustomElementName, sendExtensionMessage, setElementStyles } from "../utils"; +import { + elementIsFillableFormField, + generateRandomCustomElementName, + sendExtensionMessage, + setElementStyles, +} from "../utils"; import { AutofillOverlayElement, RedirectFocusDirection, @@ -408,7 +413,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte * @param formFieldElement - The form field element that triggered the input event. */ private triggerFormFieldInput(formFieldElement: ElementWithOpId) { - if (formFieldElement instanceof HTMLSpanElement) { + if (!elementIsFillableFormField(formFieldElement)) { return; } diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index ea81874e35..f623e0f6c9 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -7,6 +7,19 @@ import { FormFieldElement, FormElementWithAttribute, } from "../types"; +import { + elementIsDescriptionDetailsElement, + elementIsDescriptionTermElement, + elementIsFillableFormField, + elementIsFormElement, + elementIsLabelElement, + elementIsSelectElement, + elementIsSpanElement, + nodeIsFormElement, + nodeIsElement, + elementIsInputElement, + elementIsTextAreaElement, +} from "../utils"; import { AutofillOverlayContentService } from "./abstractions/autofill-overlay-content.service"; import { @@ -347,7 +360,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte tagName: this.getAttributeLowerCase(element, "tagName"), }; - if (element.tagName.toLowerCase() === "span") { + if (elementIsSpanElement(element)) { this.cacheAutofillFieldElement(index, element, autofillFieldBase); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -383,10 +396,9 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte autoCompleteType: this.getAutoCompleteAttribute(element), disabled: this.getAttributeBoolean(element, "disabled"), readonly: this.getAttributeBoolean(element, "readonly"), - selectInfo: - element.tagName.toLowerCase() === "select" - ? this.getSelectElementOptions(element as HTMLSelectElement) - : null, + 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), @@ -502,7 +514,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte let currentElement: HTMLElement | null = element; while (currentElement && currentElement !== document.documentElement) { - if (currentElement.tagName.toLowerCase() === "label") { + if (elementIsLabelElement(currentElement)) { labelElementsSet.add(currentElement); } @@ -511,10 +523,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte if ( !labelElementsSet.size && - element.parentElement?.tagName.toLowerCase() === "dd" && - element.parentElement.previousElementSibling?.tagName.toLowerCase() === "dt" + elementIsDescriptionDetailsElement(element.parentElement) && + elementIsDescriptionTermElement(element.parentElement.previousElementSibling) ) { - labelElementsSet.add(element.parentElement.previousElementSibling as HTMLElement); + labelElementsSet.add(element.parentElement.previousElementSibling); } return this.createLabelElementsTag(labelElementsSet); @@ -577,13 +589,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @private */ private getAutofillFieldMaxLength(element: FormFieldElement): number | null { - const elementTagName = element.tagName.toLowerCase(); - const elementHasMaxLengthProperty = elementTagName === "input" || elementTagName === "textarea"; + const elementHasMaxLengthProperty = + elementIsInputElement(element) || elementIsTextAreaElement(element); const elementMaxLength = - elementHasMaxLengthProperty && - (element as HTMLInputElement | HTMLTextAreaElement).maxLength > -1 - ? (element as HTMLInputElement | HTMLTextAreaElement).maxLength - : 999; + elementHasMaxLengthProperty && element.maxLength > -1 ? element.maxLength : 999; return elementHasMaxLengthProperty ? Math.min(elementMaxLength, 999) : null; } @@ -747,10 +756,9 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte // Prioritize capturing text content from elements rather than nodes. currentElement = currentElement.parentElement || currentElement.parentNode; - let siblingElement = - currentElement instanceof HTMLElement - ? currentElement.previousElementSibling - : currentElement.previousSibling; + let siblingElement = nodeIsElement(currentElement) + ? currentElement.previousElementSibling + : currentElement.previousSibling; while (siblingElement?.lastChild && !this.isNewSectionElement(siblingElement)) { siblingElement = siblingElement.lastChild; } @@ -793,13 +801,13 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @private */ private getElementValue(element: FormFieldElement): string { - if (element.tagName.toLowerCase() === "span") { + if (!elementIsFillableFormField(element)) { const spanTextContent = element.textContent || element.innerText; return spanTextContent || ""; } - const elementValue = (element as FillableFormFieldElement).value || ""; - const elementType = String((element as FillableFormFieldElement).type).toLowerCase(); + const elementValue = element.value || ""; + const elementType = String(element.type).toLowerCase(); if ("checked" in element && elementType === "checkbox") { return element.checked ? "✓" : ""; } @@ -850,7 +858,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte const formElements: Node[] = []; const formFieldElements: Node[] = []; this.queryAllTreeWalkerNodes(document.documentElement, (node: Node) => { - if ((node as HTMLFormElement).tagName?.toLowerCase() === "form") { + if (nodeIsFormElement(node)) { formElements.push(node); return true; } @@ -873,7 +881,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @private */ private isNodeFormFieldElement(node: Node): boolean { - if (!(node instanceof HTMLElement)) { + if (!nodeIsElement(node)) { return false; } @@ -904,15 +912,20 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @param {Node} node */ private getShadowRoot(node: Node): ShadowRoot | null { - if (!(node instanceof HTMLElement) || node.childNodes.length !== 0) { + if (!nodeIsElement(node) || node.childNodes.length !== 0) { return null; } + if (node.shadowRoot) { return node.shadowRoot; } if ((chrome as any).dom?.openOrClosedShadowRoot) { - return (chrome as any).dom.openOrClosedShadowRoot(node); + try { + return (chrome as any).dom.openOrClosedShadowRoot(node); + } catch (error) { + return null; + } } return (node as any).openOrClosedShadowRoot; @@ -1052,15 +1065,14 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte const mutatedElements: Node[] = []; for (let index = 0; index < nodes.length; index++) { const node = nodes[index]; - if (!(node instanceof HTMLElement)) { + if (!nodeIsElement(node)) { continue; } const autofillElementNodes = this.queryAllTreeWalkerNodes( node, (walkerNode: Node) => - (walkerNode as HTMLElement).tagName?.toLowerCase() === "form" || - this.isNodeFormFieldElement(walkerNode), + nodeIsFormElement(walkerNode) || this.isNodeFormFieldElement(walkerNode), ) as HTMLElement[]; if (autofillElementNodes.length) { @@ -1115,11 +1127,8 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte private deleteCachedAutofillElement( element: ElementWithOpId | ElementWithOpId, ) { - if ( - element.tagName.toLowerCase() === "form" && - this.autofillFormElements.has(element as ElementWithOpId) - ) { - this.autofillFormElements.delete(element as ElementWithOpId); + if (elementIsFormElement(element) && this.autofillFormElements.has(element)) { + this.autofillFormElements.delete(element); return; } @@ -1151,7 +1160,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte */ private handleAutofillElementAttributeMutation(mutation: MutationRecord) { const targetElement = mutation.target; - if (!(targetElement instanceof HTMLElement)) { + if (!nodeIsElement(targetElement)) { return; } diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.ts index 3d95de0767..32cf76f1a3 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.ts @@ -1,6 +1,13 @@ import { EVENTS, TYPE_CHECK } from "../constants"; import AutofillScript, { AutofillInsertActions, FillScript } from "../models/autofill-script"; import { FormFieldElement } from "../types"; +import { + elementIsFillableFormField, + elementIsInputElement, + elementIsSelectElement, + elementIsTextAreaElement, + nodeIsInputElement, +} from "../utils"; import { InsertAutofillContentService as InsertAutofillContentServiceInterface } from "./abstractions/insert-autofill-content.service"; import CollectAutofillContentService from "./collect-autofill-content.service"; @@ -96,7 +103,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf return Boolean( this.collectAutofillContentService.queryAllTreeWalkerNodes( document.documentElement, - (node: Node) => node instanceof HTMLInputElement && node.type === "password", + (node: Node) => nodeIsInputElement(node) && node.type === "password", false, )?.length, ); @@ -195,8 +202,8 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf */ private insertValueIntoField(element: FormFieldElement | null, value: string) { const elementCanBeReadonly = - element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement; - const elementCanBeFilled = elementCanBeReadonly || element instanceof HTMLSelectElement; + elementIsInputElement(element) || elementIsTextAreaElement(element); + const elementCanBeFilled = elementCanBeReadonly || elementIsSelectElement(element); if ( !element || @@ -207,13 +214,13 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf return; } - if (element instanceof HTMLSpanElement) { + if (!elementIsFillableFormField(element)) { this.handleInsertValueAndTriggerSimulatedEvents(element, () => (element.innerText = value)); return; } const isFillableCheckboxOrRadioElement = - element instanceof HTMLInputElement && + elementIsInputElement(element) && new Set(["checkbox", "radio"]).has(element.type) && new Set(["true", "y", "1", "yes", "✓"]).has(String(value).toLowerCase()); if (isFillableCheckboxOrRadioElement) { @@ -285,7 +292,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf */ private triggerFillAnimationOnElement(element: FormFieldElement): void { const skipAnimatingElement = - !(element instanceof HTMLSpanElement) && + elementIsFillableFormField(element) && !new Set(["email", "text", "password", "number", "tel", "url"]).has(element?.type); if (this.domElementVisibilityService.isElementHiddenByCss(element) || skipAnimatingElement) { @@ -371,6 +378,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf element.dispatchEvent(new Event(simulatedInputEvents[index], { bubbles: true })); } } + + private nodeIsElement(node: Node): node is HTMLElement { + return node.nodeType === Node.ELEMENT_NODE; + } } export default InsertAutofillContentService; diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index 94e4435e25..2644425d70 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -1,4 +1,5 @@ import { AutofillPort } from "../enums/autofill-port.enums"; +import { FillableFormFieldElement, FormFieldElement } from "../types"; /** * Generates a random string of characters that formatted as a custom element name. @@ -151,6 +152,127 @@ function setupAutofillInitDisconnectAction(windowContext: Window) { setupExtensionDisconnectAction(onDisconnectCallback); } +/** + * Identifies whether an element is a fillable form field. + * This is determined by whether the element is a form field and not a span. + * + * @param formFieldElement - The form field element to check. + */ +function elementIsFillableFormField( + formFieldElement: FormFieldElement, +): formFieldElement is FillableFormFieldElement { + return formFieldElement?.tagName.toLowerCase() !== "span"; +} + +/** + * Identifies whether an element is an instance of a specific tag name. + * + * @param element - The element to check. + * @param tagName - The tag name to check against. + */ +function elementIsInstanceOf(element: Element, tagName: string): element is T { + return element?.tagName.toLowerCase() === tagName; +} + +/** + * Identifies whether an element is a span element. + * + * @param element - The element to check. + */ +function elementIsSpanElement(element: Element): element is HTMLSpanElement { + return elementIsInstanceOf(element, "span"); +} + +/** + * Identifies whether an element is an input field. + * + * @param element - The element to check. + */ +function elementIsInputElement(element: Element): element is HTMLInputElement { + return elementIsInstanceOf(element, "input"); +} + +/** + * Identifies whether an element is a select field. + * + * @param element - The element to check. + */ +function elementIsSelectElement(element: Element): element is HTMLSelectElement { + return elementIsInstanceOf(element, "select"); +} + +/** + * Identifies whether an element is a textarea field. + * + * @param element - The element to check. + */ +function elementIsTextAreaElement(element: Element): element is HTMLTextAreaElement { + return elementIsInstanceOf(element, "textarea"); +} + +/** + * Identifies whether an element is a form element. + * + * @param element - The element to check. + */ +function elementIsFormElement(element: Element): element is HTMLFormElement { + return elementIsInstanceOf(element, "form"); +} + +/** + * Identifies whether an element is a label element. + * + * @param element - The element to check. + */ +function elementIsLabelElement(element: Element): element is HTMLLabelElement { + return elementIsInstanceOf(element, "label"); +} + +/** + * Identifies whether an element is a description details `dd` element. + * + * @param element - The element to check. + */ +function elementIsDescriptionDetailsElement(element: Element): element is HTMLElement { + return elementIsInstanceOf(element, "dd"); +} + +/** + * Identifies whether an element is a description term `dt` element. + * + * @param element - The element to check. + */ +function elementIsDescriptionTermElement(element: Element): element is HTMLElement { + return elementIsInstanceOf(element, "dt"); +} + +/** + * Identifies whether a node is an HTML element. + * + * @param node - The node to check. + */ +function nodeIsElement(node: Node): node is Element { + return node?.nodeType === Node.ELEMENT_NODE; +} + +/** + * Identifies whether a node is an input element. + * + * @param node - The node to check. + */ +function nodeIsInputElement(node: Node): node is HTMLInputElement { + return nodeIsElement(node) && elementIsInputElement(node); +} + +/** + * Identifies whether a node is a form element. + * + * @param node - The node to check. + */ +function nodeIsFormElement(node: Node): node is HTMLFormElement { + return nodeIsElement(node) && elementIsFormElement(node); +} + export { generateRandomCustomElementName, buildSvgDomElement, @@ -159,4 +281,17 @@ export { getFromLocalStorage, setupExtensionDisconnectAction, setupAutofillInitDisconnectAction, + elementIsFillableFormField, + elementIsInstanceOf, + elementIsSpanElement, + elementIsInputElement, + elementIsSelectElement, + elementIsTextAreaElement, + elementIsFormElement, + elementIsLabelElement, + elementIsDescriptionDetailsElement, + elementIsDescriptionTermElement, + nodeIsElement, + nodeIsInputElement, + nodeIsFormElement, };