/******/ (function() { // webpackBootstrap /******/ "use strict"; ;// ./src/autofill/services/dom-element-visibility.service.ts var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; class DomElementVisibilityService { constructor(inlineMenuContentService) { this.inlineMenuContentService = inlineMenuContentService; this.cachedComputedStyle = null; } /** * Checks if an element is viewable. This is done by checking if the element is within the * viewport bounds, not hidden by CSS, and not hidden behind another element. * @param element */ isElementViewable(element) { return __awaiter(this, void 0, void 0, function* () { const elementBoundingClientRect = element.getBoundingClientRect(); if (this.isElementOutsideViewportBounds(element, elementBoundingClientRect) || this.isElementHiddenByCss(element)) { return false; } return this.formFieldIsNotHiddenBehindAnotherElement(element, elementBoundingClientRect); }); } /** * Check if the target element is hidden using CSS. This is done by checking the opacity, display, * visibility, and clip-path CSS properties of the element. We also check the opacity of all * parent elements to ensure that the target element is not hidden by a parent element. * @param {HTMLElement} element * @returns {boolean} * @public */ isElementHiddenByCss(element) { this.cachedComputedStyle = null; if (this.isElementInvisible(element) || this.isElementNotDisplayed(element) || this.isElementNotVisible(element) || this.isElementClipped(element)) { return true; } let parentElement = element.parentElement; while (parentElement && parentElement !== element.ownerDocument.documentElement) { this.cachedComputedStyle = null; if (this.isElementInvisible(parentElement)) { return true; } parentElement = parentElement.parentElement; } return false; } /** * Gets the computed style of a given element, will only calculate the computed * style if the element's style has not been previously cached. * @param {HTMLElement} element * @param {string} styleProperty * @returns {string} * @private */ getElementStyle(element, styleProperty) { if (!this.cachedComputedStyle) { this.cachedComputedStyle = (element.ownerDocument.defaultView || globalThis).getComputedStyle(element); } return this.cachedComputedStyle.getPropertyValue(styleProperty); } /** * Checks if the opacity of the target element is less than 0.1. * @param {HTMLElement} element * @returns {boolean} * @private */ isElementInvisible(element) { return parseFloat(this.getElementStyle(element, "opacity")) < 0.1; } /** * Checks if the target element has a display property of none. * @param {HTMLElement} element * @returns {boolean} * @private */ isElementNotDisplayed(element) { return this.getElementStyle(element, "display") === "none"; } /** * Checks if the target element has a visibility property of hidden or collapse. * @param {HTMLElement} element * @returns {boolean} * @private */ isElementNotVisible(element) { return new Set(["hidden", "collapse"]).has(this.getElementStyle(element, "visibility")); } /** * Checks if the target element has a clip-path property that hides the element. * @param {HTMLElement} element * @returns {boolean} * @private */ isElementClipped(element) { return new Set([ "inset(50%)", "inset(100%)", "circle(0)", "circle(0px)", "circle(0px at 50% 50%)", "polygon(0 0, 0 0, 0 0, 0 0)", "polygon(0px 0px, 0px 0px, 0px 0px, 0px 0px)", ]).has(this.getElementStyle(element, "clipPath")); } /** * Checks if the target element is outside the viewport bounds. This is done by checking if the * element is too small or is overflowing the viewport bounds. * @param {HTMLElement} targetElement * @param {DOMRectReadOnly | null} targetElementBoundingClientRect * @returns {boolean} * @private */ isElementOutsideViewportBounds(targetElement, targetElementBoundingClientRect = null) { const documentElement = targetElement.ownerDocument.documentElement; const documentElementWidth = documentElement.scrollWidth; const documentElementHeight = documentElement.scrollHeight; const elementBoundingClientRect = targetElementBoundingClientRect || targetElement.getBoundingClientRect(); const elementTopOffset = elementBoundingClientRect.top - documentElement.clientTop; const elementLeftOffset = elementBoundingClientRect.left - documentElement.clientLeft; const isElementSizeInsufficient = elementBoundingClientRect.width < 10 || elementBoundingClientRect.height < 10; const isElementOverflowingLeftViewport = elementLeftOffset < 0; const isElementOverflowingRightViewport = elementLeftOffset + elementBoundingClientRect.width > documentElementWidth; const isElementOverflowingTopViewport = elementTopOffset < 0; const isElementOverflowingBottomViewport = elementTopOffset + elementBoundingClientRect.height > documentElementHeight; return (isElementSizeInsufficient || isElementOverflowingLeftViewport || isElementOverflowingRightViewport || isElementOverflowingTopViewport || isElementOverflowingBottomViewport); } /** * Checks if a passed FormField is not hidden behind another element. This is done by * checking if the element at the center point of the FormField is the FormField itself * or one of its labels. * @param {FormFieldElement} targetElement * @param {DOMRectReadOnly | null} targetElementBoundingClientRect * @returns {boolean} * @private */ formFieldIsNotHiddenBehindAnotherElement(targetElement, targetElementBoundingClientRect = null) { var _a, _b; const elementBoundingClientRect = targetElementBoundingClientRect || targetElement.getBoundingClientRect(); const elementRootNode = targetElement.getRootNode(); const rootElement = elementRootNode instanceof ShadowRoot ? elementRootNode : targetElement.ownerDocument; const elementAtCenterPoint = rootElement.elementFromPoint(elementBoundingClientRect.left + elementBoundingClientRect.width / 2, elementBoundingClientRect.top + elementBoundingClientRect.height / 2); if (elementAtCenterPoint === targetElement) { return true; } if ((_a = this.inlineMenuContentService) === null || _a === void 0 ? void 0 : _a.isElementInlineMenu(elementAtCenterPoint)) { return true; } const targetElementLabelsSet = new Set(targetElement.labels); if (targetElementLabelsSet.has(elementAtCenterPoint)) { return true; } const closestParentLabel = (_b = elementAtCenterPoint === null || elementAtCenterPoint === void 0 ? void 0 : elementAtCenterPoint.parentElement) === null || _b === void 0 ? void 0 : _b.closest("label"); return closestParentLabel ? targetElementLabelsSet.has(closestParentLabel) : false; } } /* harmony default export */ var dom_element_visibility_service = (DomElementVisibilityService); ;// ../../libs/common/src/autofill/constants/match-patterns.ts const CardExpiryDateDelimiters = ["/", "-", ".", " "]; // `CardExpiryDateDelimiters` is not intended solely for regex consumption, // so we need to format it here const ExpiryDateDelimitersPattern = "\\" + CardExpiryDateDelimiters.join("\\") // replace space character with the regex whitespace character class .replace(" ", "s"); const MonthPattern = "(([1]{1}[0-2]{1})|(0?[1-9]{1}))"; // Because we're dealing with expiry dates, we assume the year will be in current or next century (as of 2024) const ExpiryFullYearPattern = "2[0-1]{1}\\d{2}"; const DelimiterPatternExpression = new RegExp(`[${ExpiryDateDelimitersPattern}]`, "g"); const IrrelevantExpiryCharactersPatternExpression = new RegExp( // "nor digits" to ensure numbers are removed from guidance pattern, which aren't covered by ^\w `[^\\d${ExpiryDateDelimitersPattern}]`, "g"); const MonthPatternExpression = new RegExp(`^${MonthPattern}$`); const ExpiryFullYearPatternExpression = new RegExp(`^${ExpiryFullYearPattern}$`); ;// ../../libs/common/src/autofill/constants/index.ts const TYPE_CHECK = { FUNCTION: "function", NUMBER: "number", STRING: "string", }; const EVENTS = { CHANGE: "change", INPUT: "input", KEYDOWN: "keydown", KEYPRESS: "keypress", KEYUP: "keyup", BLUR: "blur", CLICK: "click", FOCUS: "focus", FOCUSIN: "focusin", FOCUSOUT: "focusout", SCROLL: "scroll", RESIZE: "resize", DOMCONTENTLOADED: "DOMContentLoaded", LOAD: "load", MESSAGE: "message", VISIBILITYCHANGE: "visibilitychange", MOUSEENTER: "mouseenter", MOUSELEAVE: "mouseleave", MOUSEUP: "mouseup", MOUSEOUT: "mouseout", SUBMIT: "submit", }; const ClearClipboardDelay = { Never: null, TenSeconds: 10, TwentySeconds: 20, ThirtySeconds: 30, OneMinute: 60, TwoMinutes: 120, FiveMinutes: 300, }; /* Ids for context menu items and messaging events */ const AUTOFILL_CARD_ID = "autofill-card"; const AUTOFILL_ID = "autofill"; const SHOW_AUTOFILL_BUTTON = "show-autofill-button"; const AUTOFILL_IDENTITY_ID = "autofill-identity"; const COPY_IDENTIFIER_ID = "copy-identifier"; const COPY_PASSWORD_ID = "copy-password"; const COPY_USERNAME_ID = "copy-username"; const COPY_VERIFICATION_CODE_ID = "copy-totp"; const CREATE_CARD_ID = "create-card"; const CREATE_IDENTITY_ID = "create-identity"; const CREATE_LOGIN_ID = "create-login"; const GENERATE_PASSWORD_ID = "generate-password"; const NOOP_COMMAND_SUFFIX = "noop"; const ROOT_ID = "root"; const SEPARATOR_ID = "separator"; const UPDATE_PASSWORD = "update-password"; const NOTIFICATION_BAR_LIFESPAN_MS = 150000; // 150 seconds const AUTOFILL_OVERLAY_HANDLE_REPOSITION = "autofill-overlay-handle-reposition-event"; const AUTOFILL_OVERLAY_HANDLE_SCROLL = "autofill-overlay-handle-scroll-event"; const UPDATE_PASSKEYS_HEADINGS_ON_SCROLL = "update-passkeys-headings-on-scroll"; const AUTOFILL_TRIGGER_FORM_FIELD_SUBMIT = "autofill-trigger-form-field-submit"; const AutofillOverlayVisibility = { Off: 0, OnButtonClick: 1, OnFieldFocus: 2, }; const BrowserClientVendors = { Chrome: "Chrome", Opera: "Opera", Edge: "Edge", Vivaldi: "Vivaldi", Unknown: "Unknown", }; const BrowserShortcutsUris = { Chrome: "chrome://extensions/shortcuts", Opera: "opera://extensions/shortcuts", Edge: "edge://extensions/shortcuts", Vivaldi: "vivaldi://extensions/shortcuts", Unknown: "https://bitwarden.com/help/keyboard-shortcuts", }; const DisablePasswordManagerUris = { Chrome: "chrome://settings/autofill", Opera: "opera://settings/autofill", Edge: "edge://settings/passwords", Vivaldi: "vivaldi://settings/autofill", Unknown: "https://bitwarden.com/help/disable-browser-autofill/", }; const ExtensionCommand = { AutofillCommand: "autofill_cmd", AutofillCard: "autofill_card", AutofillIdentity: "autofill_identity", AutofillLogin: "autofill_login", OpenAutofillOverlay: "open_autofill_overlay", GeneratePassword: "generate_password", OpenPopup: "open_popup", LockVault: "lock_vault", NoopCommand: "noop", }; const CLEAR_NOTIFICATION_LOGIN_DATA_DURATION = (/* unused pure expression or super */ null && (60 * 1000)); // 1 minute const MAX_DEEP_QUERY_RECURSION_DEPTH = 4; ;// ./src/autofill/enums/autofill-port.enum.ts const AutofillPort = { InjectedScript: "autofill-injected-script-port", }; ;// ./src/autofill/utils/index.ts var utils_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; /** * Generates a random string of characters. * * @param length - The length of the random string to generate. */ function generateRandomChars(length) { const chars = "abcdefghijklmnopqrstuvwxyz"; const randomChars = []; const randomBytes = new Uint8Array(length); globalThis.crypto.getRandomValues(randomBytes); for (let byteIndex = 0; byteIndex < randomBytes.length; byteIndex++) { const byte = randomBytes[byteIndex]; randomChars.push(chars[byte % chars.length]); } return randomChars.join(""); } /** * Polyfills the requestIdleCallback API with a setTimeout fallback. * * @param callback - The callback function to run when the browser is idle. * @param options - The options to pass to the requestIdleCallback function. */ function requestIdleCallbackPolyfill(callback, options) { if ("requestIdleCallback" in globalThis) { return globalThis.requestIdleCallback(() => callback(), options); } return globalThis.setTimeout(() => callback(), 1); } /** * Polyfills the cancelIdleCallback API with a clearTimeout fallback. * * @param id - The ID of the idle callback to cancel. */ function cancelIdleCallbackPolyfill(id) { if ("cancelIdleCallback" in globalThis) { return globalThis.cancelIdleCallback(id); } return globalThis.clearTimeout(id); } /** * Generates a random string of characters that formatted as a custom element name. */ function generateRandomCustomElementName() { const length = Math.floor(Math.random() * 5) + 8; // Between 8 and 12 characters const numHyphens = Math.min(Math.max(Math.floor(Math.random() * 4), 1), length - 1); // At least 1, maximum of 3 hyphens const hyphenIndices = []; while (hyphenIndices.length < numHyphens) { const index = Math.floor(Math.random() * (length - 1)) + 1; if (!hyphenIndices.includes(index)) { hyphenIndices.push(index); } } hyphenIndices.sort((a, b) => a - b); let randomString = ""; let prevIndex = 0; for (let index = 0; index < hyphenIndices.length; index++) { const hyphenIndex = hyphenIndices[index]; randomString = randomString + generateRandomChars(hyphenIndex - prevIndex) + "-"; prevIndex = hyphenIndex; } randomString += generateRandomChars(length - prevIndex); return randomString; } /** * Builds a DOM element from an SVG string. * * @param svgString - The SVG string to build the DOM element from. * @param ariaHidden - Determines whether the SVG should be hidden from screen readers. */ function buildSvgDomElement(svgString, ariaHidden = true) { const domParser = new DOMParser(); const svgDom = domParser.parseFromString(svgString, "image/svg+xml"); const domElement = svgDom.documentElement; domElement.setAttribute("aria-hidden", `${ariaHidden}`); return domElement; } /** * Sends a message to the extension. * * @param command - The command to send. * @param options - The options to send with the command. */ function sendExtensionMessage(command_1) { return utils_awaiter(this, arguments, void 0, function* (command, options = {}) { if (typeof browser !== "undefined" && typeof browser.runtime !== "undefined" && typeof browser.runtime.sendMessage !== "undefined") { return browser.runtime.sendMessage(Object.assign({ command }, options)); } return new Promise((resolve) => chrome.runtime.sendMessage(Object.assign({ command }, options), (response) => { if (chrome.runtime.lastError) { resolve(null); } resolve(response); })); }); } /** * Sets CSS styles on an element. * * @param element - The element to set the styles on. * @param styles - The styles to set on the element. * @param priority - Determines whether the styles should be set as important. */ function setElementStyles(element, styles, priority) { if (!element || !styles || !Object.keys(styles).length) { return; } for (const styleProperty in styles) { element.style.setProperty(styleProperty.replace(/([a-z])([A-Z])/g, "$1-$2"), // Convert camelCase to kebab-case styles[styleProperty], priority ? "important" : undefined); } } /** * Sets up a long-lived connection with the extension background * and triggers an onDisconnect event if the extension context * is invalidated. * * @param callback - Callback export function to run when the extension disconnects */ function setupExtensionDisconnectAction(callback) { const port = chrome.runtime.connect({ name: AutofillPort.InjectedScript }); const onDisconnectCallback = (disconnectedPort) => { callback(disconnectedPort); port.onDisconnect.removeListener(onDisconnectCallback); }; port.onDisconnect.addListener(onDisconnectCallback); } /** * Handles setup of the extension disconnect action for the autofill init class * in both instances where the overlay might or might not be initialized. * * @param windowContext - The global window context */ function setupAutofillInitDisconnectAction(windowContext) { if (!windowContext.bitwardenAutofillInit) { return; } const onDisconnectCallback = () => { windowContext.bitwardenAutofillInit.destroy(); delete windowContext.bitwardenAutofillInit; }; 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) { return !elementIsSpanElement(formFieldElement); } /** * 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, tagName) { return nodeIsElement(element) && element.tagName.toLowerCase() === tagName; } /** * Identifies whether an element is a span element. * * @param element - The element to check. */ function elementIsSpanElement(element) { return elementIsInstanceOf(element, "span"); } /** * Identifies whether an element is an input field. * * @param element - The element to check. */ function elementIsInputElement(element) { return elementIsInstanceOf(element, "input"); } /** * Identifies whether an element is a select field. * * @param element - The element to check. */ function elementIsSelectElement(element) { return elementIsInstanceOf(element, "select"); } /** * Identifies whether an element is a textarea field. * * @param element - The element to check. */ function elementIsTextAreaElement(element) { return elementIsInstanceOf(element, "textarea"); } /** * Identifies whether an element is a form element. * * @param element - The element to check. */ function elementIsFormElement(element) { return elementIsInstanceOf(element, "form"); } /** * Identifies whether an element is a label element. * * @param element - The element to check. */ function elementIsLabelElement(element) { return elementIsInstanceOf(element, "label"); } /** * Identifies whether an element is a description details `dd` element. * * @param element - The element to check. */ function elementIsDescriptionDetailsElement(element) { return elementIsInstanceOf(element, "dd"); } /** * Identifies whether an element is a description term `dt` element. * * @param element - The element to check. */ function elementIsDescriptionTermElement(element) { return elementIsInstanceOf(element, "dt"); } /** * Identifies whether a node is an HTML element. * * @param node - The node to check. */ function nodeIsElement(node) { if (!node) { return false; } return (node === null || node === void 0 ? void 0 : node.nodeType) === Node.ELEMENT_NODE; } /** * Identifies whether a node is an input element. * * @param node - The node to check. */ function nodeIsInputElement(node) { return nodeIsElement(node) && elementIsInputElement(node); } /** * Identifies whether a node is a form element. * * @param node - The node to check. */ function nodeIsFormElement(node) { return nodeIsElement(node) && elementIsFormElement(node); } function nodeIsTypeSubmitElement(node) { return nodeIsElement(node) && getPropertyOrAttribute(node, "type") === "submit"; } function nodeIsButtonElement(node) { return (nodeIsElement(node) && (elementIsInstanceOf(node, "button") || getPropertyOrAttribute(node, "type") === "button")); } function nodeIsAnchorElement(node) { return nodeIsElement(node) && elementIsInstanceOf(node, "a"); } /** * Returns a boolean representing the attribute value of an element. * * @param element * @param attributeName * @param checkString */ function getAttributeBoolean(element, attributeName, checkString = false) { if (checkString) { return getPropertyOrAttribute(element, attributeName) === "true"; } return Boolean(getPropertyOrAttribute(element, attributeName)); } /** * Get the value of a property or attribute from a FormFieldElement. * * @param element * @param attributeName */ function getPropertyOrAttribute(element, attributeName) { if (attributeName in element) { return element[attributeName]; } return element.getAttribute(attributeName); } /** * Throttles a callback function to run at most once every `limit` milliseconds. * * @param callback - The callback function to throttle. * @param limit - The time in milliseconds to throttle the callback. */ function throttle(callback, limit) { let waitingDelay = false; return function (...args) { if (!waitingDelay) { callback.apply(this, args); waitingDelay = true; globalThis.setTimeout(() => (waitingDelay = false), limit); } }; } /** * Debounces a callback function to run after a delay of `delay` milliseconds. * * @param callback - The callback function to debounce. * @param delay - The time in milliseconds to debounce the callback. * @param immediate - Determines whether the callback should run immediately. */ function debounce(callback, delay, immediate) { let timeout; return function (...args) { const callImmediately = !!immediate && !timeout; if (timeout) { globalThis.clearTimeout(timeout); } timeout = globalThis.setTimeout(() => { timeout = null; if (!callImmediately) { callback.apply(this, args); } }, delay); if (callImmediately) { callback.apply(this, args); } }; } /** * Gathers and normalizes keywords from a potential submit button element. Used * to verify if the element submits a login or change password form. * * @param element - The element to gather keywords from. */ function getSubmitButtonKeywordsSet(element) { const keywords = [ element.textContent, element.getAttribute("type"), element.getAttribute("value"), element.getAttribute("aria-label"), element.getAttribute("aria-labelledby"), element.getAttribute("aria-describedby"), element.getAttribute("title"), element.getAttribute("id"), element.getAttribute("name"), element.getAttribute("class"), ]; const keywordsSet = new Set(); for (let i = 0; i < keywords.length; i++) { if (typeof keywords[i] === "string") { // Iterate over all keywords metadata and split them by non-letter characters. // This ensures we check against individual words and not the entire string. keywords[i] .toLowerCase() .replace(/[-\s]/g, "") .split(/[^\p{L}]+/gu) .forEach((keyword) => { if (keyword) { keywordsSet.add(keyword); } }); } } return keywordsSet; } /** * Generates the origin and subdomain match patterns for the URL. * * @param url - The URL of the tab */ function generateDomainMatchPatterns(url) { try { const extensionUrlPattern = /^(chrome|chrome-extension|moz-extension|safari-web-extension):\/\/\/?/; if (extensionUrlPattern.test(url)) { return []; } // Add protocol to URL if it is missing to allow for parsing the hostname correctly const urlPattern = /^(https?|file):\/\/\/?/; if (!urlPattern.test(url)) { url = `https://${url}`; } let protocolGlob = "*://"; if (url.startsWith("file:///")) { protocolGlob = "*:///"; // File URLs require three slashes to be a valid match pattern } const parsedUrl = new URL(url); const originMatchPattern = `${protocolGlob}${parsedUrl.hostname}/*`; const splitHost = parsedUrl.hostname.split("."); const domain = splitHost.slice(-2).join("."); const subDomainMatchPattern = `${protocolGlob}*.${domain}/*`; return [originMatchPattern, subDomainMatchPattern]; } catch (_a) { return []; } } /** * Determines if the status code of the web response is invalid. An invalid status code is * any status code that is not in the 200-299 range. * * @param statusCode - The status code of the web response */ function isInvalidResponseStatusCode(statusCode) { return statusCode < 200 || statusCode >= 300; } /** * Determines if the current context is within a sandboxed iframe. */ function currentlyInSandboxedIframe() { var _a, _b; if (String(self.origin).toLowerCase() === "null" || globalThis.location.hostname === "") { return true; } const sandbox = (_b = (_a = globalThis.frameElement) === null || _a === void 0 ? void 0 : _a.getAttribute) === null || _b === void 0 ? void 0 : _b.call(_a, "sandbox"); // No frameElement or sandbox attribute means not sandboxed if (sandbox === null || sandbox === undefined) { return false; } // An empty string means fully sandboxed if (sandbox === "") { return true; } const tokens = new Set(sandbox.toLowerCase().split(" ")); return !["allow-scripts", "allow-same-origin"].every((token) => tokens.has(token)); } /** * This object allows us to map a special character to a key name. The key name is used * in gathering the i18n translation of the written version of the special character. */ const specialCharacterToKeyMap = { " ": "spaceCharacterDescriptor", "~": "tildeCharacterDescriptor", "`": "backtickCharacterDescriptor", "!": "exclamationCharacterDescriptor", "@": "atSignCharacterDescriptor", "#": "hashSignCharacterDescriptor", $: "dollarSignCharacterDescriptor", "%": "percentSignCharacterDescriptor", "^": "caretCharacterDescriptor", "&": "ampersandCharacterDescriptor", "*": "asteriskCharacterDescriptor", "(": "parenLeftCharacterDescriptor", ")": "parenRightCharacterDescriptor", "-": "hyphenCharacterDescriptor", _: "underscoreCharacterDescriptor", "+": "plusCharacterDescriptor", "=": "equalsCharacterDescriptor", "{": "braceLeftCharacterDescriptor", "}": "braceRightCharacterDescriptor", "[": "bracketLeftCharacterDescriptor", "]": "bracketRightCharacterDescriptor", "|": "pipeCharacterDescriptor", "\\": "backSlashCharacterDescriptor", ":": "colonCharacterDescriptor", ";": "semicolonCharacterDescriptor", '"': "doubleQuoteCharacterDescriptor", "'": "singleQuoteCharacterDescriptor", "<": "lessThanCharacterDescriptor", ">": "greaterThanCharacterDescriptor", ",": "commaCharacterDescriptor", ".": "periodCharacterDescriptor", "?": "questionCharacterDescriptor", "/": "forwardSlashCharacterDescriptor", }; /** * Determines if the current rect values are not all 0. */ function rectHasSize(rect) { if (rect.right > 0 && rect.left > 0 && rect.top > 0 && rect.bottom > 0) { return true; } return false; } /** * Checks if all the values corresponding to the specified keys in an object are null. * If no keys are specified, checks all keys in the object. * * @param obj - The object to check. * @param keys - An optional array of keys to check in the object. Defaults to all keys. * @returns Returns true if all values for the specified keys (or all keys if none are provided) are null; otherwise, false. */ function areKeyValuesNull(obj, keys) { const keysToCheck = keys && keys.length > 0 ? keys : Object.keys(obj); return keysToCheck.every((key) => obj[key] == null); } ;// ./src/autofill/services/dom-query.service.ts var dom_query_service_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; class DomQueryService { constructor() { this.ignoredTreeWalkerNodes = new Set([ "svg", "script", "noscript", "head", "style", "link", "meta", "title", "base", "img", "picture", "video", "audio", "object", "source", "track", "param", "map", "area", ]); /** * Checks if the page contains any shadow DOM elements. */ this.checkPageContainsShadowDom = () => { this.pageContainsShadowDom = this.queryShadowRoots(globalThis.document.body, true).length > 0; }; void this.init(); } /** * Sets up a query that will trigger a deepQuery of the DOM, querying all elements that match the given query string. * If the deepQuery fails or reaches a max recursion depth, it will fall back to a treeWalker query. * * @param root - The root element to start the query from * @param queryString - The query string to match elements against * @param treeWalkerFilter - The filter callback to use for the treeWalker query * @param mutationObserver - The MutationObserver to use for observing shadow roots * @param forceDeepQueryAttempt - Whether to force a deep query attempt * @param ignoredTreeWalkerNodesOverride - An optional set of node names to ignore when using the treeWalker strategy */ query(root, queryString, treeWalkerFilter, mutationObserver, forceDeepQueryAttempt, ignoredTreeWalkerNodesOverride) { const ignoredTreeWalkerNodes = ignoredTreeWalkerNodesOverride || this.ignoredTreeWalkerNodes; if (!forceDeepQueryAttempt) { return this.queryAllTreeWalkerNodes(root, treeWalkerFilter, ignoredTreeWalkerNodes, mutationObserver); } try { return this.deepQueryElements(root, queryString, mutationObserver); } catch (_a) { return this.queryAllTreeWalkerNodes(root, treeWalkerFilter, ignoredTreeWalkerNodes, mutationObserver); } } /** * Initializes the DomQueryService, checking for the presence of shadow DOM elements on the page. */ init() { return dom_query_service_awaiter(this, void 0, void 0, function* () { if (globalThis.document.readyState === "complete") { this.checkPageContainsShadowDom(); return; } globalThis.addEventListener(EVENTS.LOAD, this.checkPageContainsShadowDom); }); } /** * Queries all elements in the DOM that match the given query string. * Also, recursively queries all shadow roots for the element. * * @param root - The root element to start the query from * @param queryString - The query string to match elements against * @param mutationObserver - The MutationObserver to use for observing shadow roots */ deepQueryElements(root, queryString, mutationObserver) { let elements = this.queryElements(root, queryString); const shadowRoots = this.recursivelyQueryShadowRoots(root); for (let index = 0; index < shadowRoots.length; index++) { const shadowRoot = shadowRoots[index]; elements = elements.concat(this.queryElements(shadowRoot, queryString)); if (mutationObserver) { mutationObserver.observe(shadowRoot, { attributes: true, childList: true, subtree: true, }); } } return elements; } /** * Queries the DOM for elements based on the given query string. * * @param root - The root element to start the query from * @param queryString - The query string to match elements against */ queryElements(root, queryString) { if (!root.querySelector(queryString)) { return []; } return Array.from(root.querySelectorAll(queryString)); } /** * Recursively queries all shadow roots found within the given root element. * Will also set up a mutation observer on the shadow root if the * `isObservingShadowRoot` parameter is set to true. * * @param root - The root element to start the query from * @param depth - The depth of the recursion */ recursivelyQueryShadowRoots(root, depth = 0) { if (!this.pageContainsShadowDom) { return []; } if (depth >= MAX_DEEP_QUERY_RECURSION_DEPTH) { throw new Error("Max recursion depth reached"); } let shadowRoots = this.queryShadowRoots(root); for (let index = 0; index < shadowRoots.length; index++) { const shadowRoot = shadowRoots[index]; shadowRoots = shadowRoots.concat(this.recursivelyQueryShadowRoots(shadowRoot, depth + 1)); } return shadowRoots; } /** * Queries any immediate shadow roots found within the given root element. * * @param root - The root element to start the query from * @param returnSingleShadowRoot - Whether to return a single shadow root or an array of shadow roots */ queryShadowRoots(root, returnSingleShadowRoot = false) { if (!root) { return []; } const shadowRoots = []; const potentialShadowRoots = root.querySelectorAll(":defined"); for (let index = 0; index < potentialShadowRoots.length; index++) { const shadowRoot = this.getShadowRoot(potentialShadowRoots[index]); if (!shadowRoot) { continue; } shadowRoots.push(shadowRoot); if (returnSingleShadowRoot) { break; } } return shadowRoots; } /** * Attempts to get the ShadowRoot of the passed node. If support for the * extension based openOrClosedShadowRoot API is available, it will be used. * Will return null if the node is not an HTMLElement or if the node has * child nodes. * * @param {Node} node */ getShadowRoot(node) { var _a; if (!nodeIsElement(node)) { return null; } if (node.shadowRoot) { return node.shadowRoot; } if ((_a = chrome.dom) === null || _a === void 0 ? void 0 : _a.openOrClosedShadowRoot) { try { return chrome.dom.openOrClosedShadowRoot(node); } catch (_b) { return null; } } // Firefox-specific equivalent of `openOrClosedShadowRoot` return node.openOrClosedShadowRoot; } /** * Queries the DOM for all the nodes that match the given filter callback * and returns a collection of nodes. * @param rootNode * @param filterCallback * @param ignoredTreeWalkerNodes * @param mutationObserver */ queryAllTreeWalkerNodes(rootNode, filterCallback, ignoredTreeWalkerNodes, mutationObserver) { const treeWalkerQueryResults = []; this.buildTreeWalkerNodesQueryResults(rootNode, treeWalkerQueryResults, filterCallback, ignoredTreeWalkerNodes, mutationObserver); return treeWalkerQueryResults; } /** * Recursively builds a collection of nodes that match the given filter callback. * If a node has a ShadowRoot, it will be observed for mutations. * * @param rootNode * @param treeWalkerQueryResults * @param filterCallback * @param ignoredTreeWalkerNodes * @param mutationObserver */ buildTreeWalkerNodesQueryResults(rootNode, treeWalkerQueryResults, filterCallback, ignoredTreeWalkerNodes, mutationObserver) { const treeWalker = document === null || document === void 0 ? void 0 : document.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT, (node) => { var _a; return ignoredTreeWalkerNodes.has((_a = node.nodeName) === null || _a === void 0 ? void 0 : _a.toLowerCase()) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; }); let currentNode = treeWalker === null || treeWalker === void 0 ? void 0 : treeWalker.currentNode; while (currentNode) { if (filterCallback(currentNode)) { treeWalkerQueryResults.push(currentNode); } const nodeShadowRoot = this.getShadowRoot(currentNode); if (nodeShadowRoot) { if (mutationObserver) { mutationObserver.observe(nodeShadowRoot, { attributes: true, childList: true, subtree: true, }); } this.buildTreeWalkerNodesQueryResults(nodeShadowRoot, treeWalkerQueryResults, filterCallback, ignoredTreeWalkerNodes, mutationObserver); } currentNode = treeWalker === null || treeWalker === void 0 ? void 0 : treeWalker.nextNode(); } } } ;// ./src/autofill/services/collect-autofill-content.service.ts var collect_autofill_content_service_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; class CollectAutofillContentService { constructor(domElementVisibilityService, domQueryService, autofillOverlayContentService) { this.domElementVisibilityService = domElementVisibilityService; this.domQueryService = domQueryService; this.autofillOverlayContentService = autofillOverlayContentService; this.sendExtensionMessage = sendExtensionMessage; this.getAttributeBoolean = getAttributeBoolean; this.getPropertyOrAttribute = getPropertyOrAttribute; this.noFieldsFound = false; this.domRecentlyMutated = true; this._autofillFormElements = new Map(); this.autofillFieldElements = new Map(); this.currentLocationHref = ""; this.elementInitializingIntersectionObserver = new Set(); this.mutationsQueue = []; this.ownedExperienceTagNames = []; this.updateAfterMutationTimeout = 1000; this.nonInputFormFieldTags = new Set(["textarea", "select"]); this.ignoredInputTypes = new Set([ "hidden", "submit", "reset", "button", "image", "file", ]); /** * Builds an AutofillField object from the given form element. Will only return * shared field values if the element is a span element. Will not return any label * values if the element is a hidden input element. * * @param element - The form field element to build the AutofillField object from * @param index - The index of the form field element */ this.buildAutofillFieldItem = (element, index) => collect_autofill_content_service_awaiter(this, void 0, void 0, function* () { var _a; if (element.closest("button[type='submit']")) { return null; } element.opid = `__${index}`; const existingAutofillField = this.autofillFieldElements.get(element); if (index >= 0 && existingAutofillField) { existingAutofillField.opid = element.opid; existingAutofillField.elementNumber = index; this.autofillFieldElements.set(element, existingAutofillField); return existingAutofillField; } const autofillFieldBase = { opid: element.opid, elementNumber: index, maxLength: this.getAutofillFieldMaxLength(element), viewable: yield 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"), tagName: this.getAttributeLowerCase(element, "tagName"), dataSetValues: this.getDataSetValues(element), }; if (!autofillFieldBase.viewable) { this.elementInitializingIntersectionObserver.add(element); (_a = this.intersectionObserver) === null || _a === void 0 ? void 0 : _a.observe(element); } if (elementIsSpanElement(element)) { this.cacheAutofillFieldElement(index, element, autofillFieldBase); return autofillFieldBase; } let autofillFieldLabels = {}; const elementType = this.getAttributeLowerCase(element, "type"); if (elementType !== "hidden") { autofillFieldLabels = { "label-tag": this.createAutofillFieldLabelTag(element), "label-data": this.getPropertyOrAttribute(element, "data-label"), "label-aria": this.getPropertyOrAttribute(element, "aria-label"), "label-top": this.createAutofillFieldTopLabel(element), "label-right": this.createAutofillFieldRightLabel(element), "label-left": this.createAutofillFieldLeftLabel(element), placeholder: this.getPropertyOrAttribute(element, "placeholder"), }; } const fieldFormElement = element.form; const autofillField = Object.assign(Object.assign(Object.assign({}, autofillFieldBase), autofillFieldLabels), { rel: this.getPropertyOrAttribute(element, "rel"), type: elementType, value: this.getElementValue(element), checked: this.getAttributeBoolean(element, "checked"), autoCompleteType: this.getAutoCompleteAttribute(element), disabled: this.getAttributeBoolean(element, "disabled"), readonly: this.getAttributeBoolean(element, "readonly"), selectInfo: elementIsSelectElement(element) ? this.getSelectElementOptions(element) : 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") }); this.cacheAutofillFieldElement(index, element, autofillField); return autofillField; }); /** * Map over all the label elements and creates a * string of the text content of each label element. * @param {Set} labelElementsSet * @returns {string} * @private */ this.createLabelElementsTag = (labelElementsSet) => { return Array.from(labelElementsSet) .map((labelElement) => { const textContent = labelElement ? labelElement.textContent || labelElement.innerText : null; return this.trimAndRemoveNonPrintableText(textContent || ""); }) .join(""); }; this.setupInitialTopLayerListeners = () => { var _a; const unownedTopLayerItems = (_a = this.autofillOverlayContentService) === null || _a === void 0 ? void 0 : _a.getUnownedTopLayerItems(true); if (unownedTopLayerItems === null || unownedTopLayerItems === void 0 ? void 0 : unownedTopLayerItems.length) { for (const unownedElement of unownedTopLayerItems) { if (this.shouldListenToTopLayerCandidate(unownedElement)) { this.setupTopLayerCandidateListener(unownedElement); } } } }; /** * Handles observed DOM mutations and identifies if a mutation is related to * an autofill element. If so, it will update the autofill element data. * @param {MutationRecord[]} mutations * @private */ this.handleMutationObserverMutation = (mutations) => { if (this.currentLocationHref !== globalThis.location.href) { this.handleWindowLocationMutation(); return; } if (!this.mutationsQueue.length) { requestIdleCallbackPolyfill(debounce(this.processMutations, 100), { timeout: 500 }); } this.mutationsQueue.push(mutations); }; /** * Handles the processing of all mutations in the mutations queue. Will trigger * within an idle callback to help with performance and prevent excessive updates. */ this.processMutations = () => { const queueLength = this.mutationsQueue.length; for (let queueIndex = 0; queueIndex < queueLength; queueIndex++) { const mutations = this.mutationsQueue[queueIndex]; const processMutationRecords = () => { this.processMutationRecords(mutations); if (queueIndex === queueLength - 1 && this.domRecentlyMutated) { this.updateAutofillElementsAfterMutation(); } }; requestIdleCallbackPolyfill(processMutationRecords, { timeout: 500 }); } this.mutationsQueue = []; }; this.setupTopLayerCandidateListener = (element) => { const ownedTags = this.autofillOverlayContentService.getOwnedInlineMenuTagNames() || []; this.ownedExperienceTagNames = ownedTags; if (!ownedTags.includes(element.tagName)) { element.addEventListener("toggle", (event) => { if (event.newState === "open") { // Add a slight delay (but faster than a user's reaction), to ensure the layer // positioning happens after any triggered toggle has completed. setTimeout(this.autofillOverlayContentService.refreshMenuLayerPosition, 100); } }); } }; this.isPopoverAttribute = (attr) => { const popoverAttributes = new Set(["popover", "popovertarget", "popovertargetaction"]); return attr && popoverAttributes.has(attr.toLowerCase()); }; this.shouldListenToTopLayerCandidate = (element) => { return (!this.ownedExperienceTagNames.includes(element.tagName) && (element.tagName === "DIALOG" || Array.from(element.attributes || []).some((attribute) => this.isPopoverAttribute(attribute.name)))); }; /** * Checks if a mutation record is related features that utilize the top layer. * If so, it then calls `setupTopLayerElementListener` for future event * listening on the relevant element. * * @param mutation - The MutationRecord to check */ this.handleTopLayerChanges = (mutation) => { var _a; // Check attribute mutations if (mutation.type === "attributes" && this.isPopoverAttribute(mutation.attributeName)) { this.setupTopLayerCandidateListener(mutation.target); } // Check added nodes for dialog or popover attributes if (mutation.type === "childList" && ((_a = mutation.addedNodes) === null || _a === void 0 ? void 0 : _a.length) > 0) { for (const node of mutation.addedNodes) { const mutationElement = node; if (this.shouldListenToTopLayerCandidate(mutationElement)) { this.setupTopLayerCandidateListener(mutationElement); } } } return; }; /** * Handles observed form field elements that are not viewable in the viewport. * Will re-evaluate the visibility of the element and set up the autofill * overlay listeners on the field if it is viewable. * * @param entries - The entries observed by the IntersectionObserver */ this.handleFormElementIntersection = (entries) => collect_autofill_content_service_awaiter(this, void 0, void 0, function* () { var _a; for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) { const entry = entries[entryIndex]; const formFieldElement = entry.target; if (this.elementInitializingIntersectionObserver.has(formFieldElement)) { this.elementInitializingIntersectionObserver.delete(formFieldElement); continue; } const cachedAutofillFieldElement = this.autofillFieldElements.get(formFieldElement); if (!cachedAutofillFieldElement) { this.intersectionObserver.unobserve(entry.target); continue; } const isViewable = yield this.domElementVisibilityService.isElementViewable(formFieldElement); if (!isViewable) { continue; } cachedAutofillFieldElement.viewable = true; this.setupOverlayOnField(formFieldElement, cachedAutofillFieldElement); (_a = this.intersectionObserver) === null || _a === void 0 ? void 0 : _a.unobserve(entry.target); } }); let inputQuery = "input:not([data-bwignore])"; for (const type of this.ignoredInputTypes) { inputQuery += `:not([type="${type}"])`; } this.formFieldQueryString = `${inputQuery}, textarea:not([data-bwignore]), select:not([data-bwignore]), span[data-bwautofill]`; } get autofillFormElements() { return this._autofillFormElements; } /** * Builds the data for all forms and fields found within the page DOM. * Sets up a mutation observer to verify DOM changes and returns early * with cached data if no changes are detected. * @returns {Promise} * @public */ getPageDetails() { return collect_autofill_content_service_awaiter(this, void 0, void 0, function* () { // Set up listeners on top-layer candidates that predate Mutation Observer setup this.setupInitialTopLayerListeners(); if (!this.mutationObserver) { this.setupMutationObserver(); } if (!this.intersectionObserver) { this.setupIntersectionObserver(); } if (!this.domRecentlyMutated && this.noFieldsFound) { return this.getFormattedPageDetails({}, []); } if (!this.domRecentlyMutated && this.autofillFieldElements.size) { this.updateCachedAutofillFieldVisibility(); return this.getFormattedPageDetails(this.getFormattedAutofillFormsData(), this.getFormattedAutofillFieldsData()); } const { formElements, formFieldElements } = this.queryAutofillFormAndFieldElements(); const autofillFormsData = this.buildAutofillFormsData(formElements); const autofillFieldsData = (yield this.buildAutofillFieldsData(formFieldElements)).filter((field) => !!field); this.sortAutofillFieldElementsMap(); if (!autofillFieldsData.length) { this.noFieldsFound = true; } this.domRecentlyMutated = false; const pageDetails = this.getFormattedPageDetails(autofillFormsData, autofillFieldsData); this.setupOverlayListeners(pageDetails); return pageDetails; }); } /** * Find an AutofillField element by its opid, will only return the first * element if there are multiple elements with the same opid. If no * element is found, null will be returned. * @param {string} opid * @returns {FormFieldElement | null} */ getAutofillFieldElementByOpid(opid) { const cachedFormFieldElements = Array.from(this.autofillFieldElements.keys()); const formFieldElements = (cachedFormFieldElements === null || cachedFormFieldElements === void 0 ? void 0 : cachedFormFieldElements.length) ? cachedFormFieldElements : this.getAutofillFieldElements(); const fieldElementsWithOpid = formFieldElements.filter((fieldElement) => fieldElement.opid === opid); if (!fieldElementsWithOpid.length) { const elementIndex = parseInt(opid.split("__")[1], 10); return formFieldElements[elementIndex] || null; } if (fieldElementsWithOpid.length > 1) { // eslint-disable-next-line no-console console.warn(`More than one element found with opid ${opid}`); } return fieldElementsWithOpid[0]; } /** * Sorts the AutofillFieldElements map by the elementNumber property. * @private */ sortAutofillFieldElementsMap() { if (!this.autofillFieldElements.size) { return; } this.autofillFieldElements = new Map([...this.autofillFieldElements].sort((a, b) => a[1].elementNumber - b[1].elementNumber)); } /** * Formats and returns the AutofillPageDetails object * * @param autofillFormsData - The data for all the forms found in the page * @param autofillFieldsData - The data for all the fields found in the page */ getFormattedPageDetails(autofillFormsData, autofillFieldsData) { return { title: document.title, url: (document.defaultView || globalThis).location.href, documentUrl: document.location.href, forms: autofillFormsData, fields: autofillFieldsData, collectedTimestamp: Date.now(), }; } /** * Re-checks the visibility for all form fields and updates the * cached data to reflect the most recent visibility state. * * @private */ updateCachedAutofillFieldVisibility() { this.autofillFieldElements.forEach((autofillField, element) => collect_autofill_content_service_awaiter(this, void 0, void 0, function* () { const previouslyViewable = autofillField.viewable; autofillField.viewable = yield this.domElementVisibilityService.isElementViewable(element); if (!previouslyViewable && autofillField.viewable) { this.setupOverlayOnField(element, autofillField); } })); } /** * Queries the DOM for all the forms elements and * returns a collection of AutofillForm objects. * @returns {Record} * @private */ buildAutofillFormsData(formElements) { for (let index = 0; index < formElements.length; index++) { const formElement = formElements[index]; formElement.opid = `__form__${index}`; const existingAutofillForm = this._autofillFormElements.get(formElement); if (existingAutofillForm) { existingAutofillForm.opid = formElement.opid; this._autofillFormElements.set(formElement, existingAutofillForm); continue; } 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"), }); } return this.getFormattedAutofillFormsData(); } /** * Returns the action attribute of the form element. If the action attribute * is a relative path, it will be converted to an absolute path. * @param {ElementWithOpId} element * @returns {string} * @private */ getFormActionAttribute(element) { return new URL(this.getPropertyOrAttribute(element, "action"), globalThis.location.href).href; } /** * Iterates over all known form elements and returns an AutofillForm object * containing a key value pair of the form element's opid and the form data. * @returns {Record} * @private */ getFormattedAutofillFormsData() { const autofillForms = {}; const autofillFormElements = Array.from(this._autofillFormElements); for (let index = 0; index < autofillFormElements.length; index++) { const [formElement, autofillForm] = autofillFormElements[index]; autofillForms[formElement.opid] = autofillForm; } return autofillForms; } /** * Queries the DOM for all the field elements and * returns a list of AutofillField objects. * @returns {Promise} * @private */ buildAutofillFieldsData(formFieldElements) { return collect_autofill_content_service_awaiter(this, void 0, void 0, function* () { // Maximum number of form fields to process for autofill to prevent performance issues on pages with excessive fields const autofillFieldsLimit = 200; const autofillFieldElements = this.getAutofillFieldElements(autofillFieldsLimit, formFieldElements); const autofillFieldDataPromises = autofillFieldElements.map(this.buildAutofillFieldItem); return Promise.all(autofillFieldDataPromises); }); } /** * Queries the DOM for all the field elements that can be autofilled, * and returns a list limited to the given `fieldsLimit` number that * is ordered by priority. * @param {number} fieldsLimit - The maximum number of fields to return * @param {FormFieldElement[]} previouslyFoundFormFieldElements - The list of all the field elements * @returns {FormFieldElement[]} * @private */ getAutofillFieldElements(fieldsLimit, previouslyFoundFormFieldElements) { var _a; let formFieldElements = previouslyFoundFormFieldElements; if (!formFieldElements) { formFieldElements = this.domQueryService.query(globalThis.document.documentElement, this.formFieldQueryString, (node) => this.isNodeFormFieldElement(node), this.mutationObserver); } if (!fieldsLimit || formFieldElements.length <= fieldsLimit) { return formFieldElements; } const priorityFormFields = []; const unimportantFormFields = []; const unimportantFieldTypesSet = new Set(["checkbox", "radio"]); for (const element of formFieldElements) { if (priorityFormFields.length >= fieldsLimit) { return priorityFormFields; } const fieldType = (_a = this.getPropertyOrAttribute(element, "type")) === null || _a === void 0 ? void 0 : _a.toLowerCase(); if (unimportantFieldTypesSet.has(fieldType)) { unimportantFormFields.push(element); continue; } priorityFormFields.push(element); } const numberUnimportantFieldsToInclude = fieldsLimit - priorityFormFields.length; for (let index = 0; index < numberUnimportantFieldsToInclude; index++) { priorityFormFields.push(unimportantFormFields[index]); } return priorityFormFields; } /** * Caches the autofill field element and its data. * * @param index - The index of the autofill field element * @param element - The autofill field element to cache * @param autofillFieldData - The autofill field data to cache */ cacheAutofillFieldElement(index, element, autofillFieldData) { // Always cache the element, even if index is -1 (for dynamically added fields) this.autofillFieldElements.set(element, autofillFieldData); } /** * Identifies the autocomplete attribute associated with an element and returns * the value of the attribute if it is not set to "off". * @param {ElementWithOpId} element * @returns {string} * @private */ getAutoCompleteAttribute(element) { return (this.getPropertyOrAttribute(element, "x-autocompletetype") || this.getPropertyOrAttribute(element, "autocompletetype") || this.getPropertyOrAttribute(element, "autocomplete")); } /** * Returns the attribute of an element as a lowercase value. * @param {ElementWithOpId} element * @param {string} attributeName * @returns {string} * @private */ getAttributeLowerCase(element, attributeName) { var _a; return (_a = this.getPropertyOrAttribute(element, attributeName)) === null || _a === void 0 ? void 0 : _a.toLowerCase(); } /** * Returns the value of an element's property or attribute. * @returns {AutofillField[]} * @private */ getFormattedAutofillFieldsData() { return Array.from(this.autofillFieldElements.values()); } /** * Creates a label tag used to autofill the element pulled from a label * associated with the element's id, name, parent element or from an * associated description term element if no other labels can be found. * Returns a string containing all the `textContent` or `innerText` * values of the label elements. * @param {FillableFormFieldElement} element * @returns {string} * @private */ createAutofillFieldLabelTag(element) { var _a; const labelElementsSet = new Set(element.labels); if (labelElementsSet.size) { return this.createLabelElementsTag(labelElementsSet); } const labelElements = this.queryElementLabels(element); for (let labelIndex = 0; labelIndex < (labelElements === null || labelElements === void 0 ? void 0 : labelElements.length); labelIndex++) { labelElementsSet.add(labelElements[labelIndex]); } let currentElement = element; while (currentElement && currentElement !== document.documentElement) { if (elementIsLabelElement(currentElement)) { labelElementsSet.add(currentElement); } currentElement = (_a = currentElement.parentElement) === null || _a === void 0 ? void 0 : _a.closest("label"); } if (!labelElementsSet.size && elementIsDescriptionDetailsElement(element.parentElement) && elementIsDescriptionTermElement(element.parentElement.previousElementSibling)) { labelElementsSet.add(element.parentElement.previousElementSibling); } return this.createLabelElementsTag(labelElementsSet); } /** * Queries the DOM for label elements associated with the given element * by id or name. Returns a NodeList of label elements or null if none * are found. * @param {FillableFormFieldElement} element * @returns {NodeListOf | null} * @private */ queryElementLabels(element) { let labelQuerySelectors = element.id ? `label[for="${element.id}"]` : ""; if (element.name) { const forElementNameSelector = `label[for="${element.name}"]`; labelQuerySelectors = labelQuerySelectors ? `${labelQuerySelectors}, ${forElementNameSelector}` : forElementNameSelector; } if (!labelQuerySelectors) { return null; } return element.getRootNode().querySelectorAll(labelQuerySelectors.replace(/\n/g, "")); } /** * Gets the maxLength property of the passed FormFieldElement and * returns the value or null if the element does not have a * maxLength property. If the element has a maxLength property * greater than 999, it will return 999. * @param {FormFieldElement} element * @returns {number | null} * @private */ getAutofillFieldMaxLength(element) { const elementHasMaxLengthProperty = elementIsInputElement(element) || elementIsTextAreaElement(element); const elementMaxLength = elementHasMaxLengthProperty && element.maxLength > -1 ? element.maxLength : 999; return elementHasMaxLengthProperty ? Math.min(elementMaxLength, 999) : null; } /** * Iterates over the next siblings of the passed element and * returns a string of the text content of each element. Will * stop iterating if it encounters a new section element. * @param {FormFieldElement} element * @returns {string} * @private */ createAutofillFieldRightLabel(element) { const labelTextContent = []; let currentElement = element; while (currentElement && currentElement.nextSibling) { currentElement = currentElement.nextSibling; if (this.isNewSectionElement(currentElement)) { break; } const textContent = this.getTextContentFromElement(currentElement); if (textContent) { labelTextContent.push(textContent); } } return labelTextContent.join(""); } /** * Recursively gets the text content from an element's previous siblings * and returns a string of the text content of each element. * @param {FormFieldElement} element * @returns {string} * @private */ createAutofillFieldLeftLabel(element) { const labelTextContent = this.recursivelyGetTextFromPreviousSiblings(element); return labelTextContent.reverse().join(""); } /** * Assumes that the input elements that are to be autofilled are within a * table structure. Queries the previous sibling of the parent row that * the input element is in and returns the text content of the cell that * is in the same column as the input element. * @param {FormFieldElement} element * @returns {string | null} * @private */ createAutofillFieldTopLabel(element) { var _a, _b; const tableDataElement = element.closest("td"); if (!tableDataElement) { return null; } const tableDataElementIndex = tableDataElement.cellIndex; if (tableDataElementIndex < 0) { return null; } const parentSiblingTableRowElement = (_a = tableDataElement.closest("tr")) === null || _a === void 0 ? void 0 : _a.previousElementSibling; return ((_b = parentSiblingTableRowElement === null || parentSiblingTableRowElement === void 0 ? void 0 : parentSiblingTableRowElement.cells) === null || _b === void 0 ? void 0 : _b.length) > tableDataElementIndex ? this.getTextContentFromElement(parentSiblingTableRowElement.cells[tableDataElementIndex]) : null; } /** * Check if the element's tag indicates that a transition to a new section of the * page is occurring. If so, we should not use the element or its children in order * to get autofill context for the previous element. * @param {HTMLElement} currentElement * @returns {boolean} * @private */ isNewSectionElement(currentElement) { if (!currentElement) { return true; } const transitionalElementTagsSet = new Set([ "html", "body", "button", "form", "head", "iframe", "input", "option", "script", "select", "table", "textarea", ]); return ("tagName" in currentElement && transitionalElementTagsSet.has(currentElement.tagName.toLowerCase())); } /** * Gets the text content from a passed element, regardless of whether it is a * text node, an element node or an HTMLElement. * @param {Node | HTMLElement} element * @returns {string} * @private */ getTextContentFromElement(element) { if (element.nodeType === Node.TEXT_NODE) { return this.trimAndRemoveNonPrintableText(element.nodeValue); } return this.trimAndRemoveNonPrintableText(element.textContent || element.innerText); } /** * Removes non-printable characters from the passed text * content and trims leading and trailing whitespace. * @param {string} textContent * @returns {string} * @private */ trimAndRemoveNonPrintableText(textContent) { return (textContent || "") .replace(/\p{C}+|\s+/gu, " ") // Strip out non-printable characters and replace multiple spaces with a single space .trim(); // Trim leading and trailing whitespace } /** * Get the text content from the previous siblings of the element. If * no text content is found, recursively get the text content from the * previous siblings of the parent element. * @param {FormFieldElement} element * @returns {string[]} * @private */ recursivelyGetTextFromPreviousSiblings(element) { const textContentItems = []; let currentElement = element; while (currentElement && currentElement.previousSibling) { // Ensure we are capturing text content from nodes and elements. currentElement = currentElement.previousSibling; if (this.isNewSectionElement(currentElement)) { return textContentItems; } const textContent = this.getTextContentFromElement(currentElement); if (textContent) { textContentItems.push(textContent); } } if (!currentElement || textContentItems.length) { return textContentItems; } // Prioritize capturing text content from elements rather than nodes. currentElement = currentElement.parentElement || currentElement.parentNode; if (!currentElement) { return textContentItems; } let siblingElement = nodeIsElement(currentElement) ? currentElement.previousElementSibling : currentElement.previousSibling; while ((siblingElement === null || siblingElement === void 0 ? void 0 : siblingElement.lastChild) && !this.isNewSectionElement(siblingElement)) { siblingElement = siblingElement.lastChild; } if (this.isNewSectionElement(siblingElement)) { return textContentItems; } const textContent = this.getTextContentFromElement(siblingElement); if (textContent) { textContentItems.push(textContent); return textContentItems; } return this.recursivelyGetTextFromPreviousSiblings(siblingElement); } /** * Gets the value of the element. If the element is a checkbox, returns a checkmark if the * checkbox is checked, or an empty string if it is not checked. If the element is a hidden * input, returns the value of the input if it is less than 254 characters, or a truncated * value if it is longer than 254 characters. * @param {FormFieldElement} element * @returns {string} * @private */ getElementValue(element) { if (!elementIsFillableFormField(element)) { const spanTextContent = element.textContent || element.innerText; return spanTextContent || ""; } const elementValue = element.value || ""; const elementType = String(element.type).toLowerCase(); if ("checked" in element && elementType === "checkbox") { return element.checked ? "✓" : ""; } if (elementType === "hidden") { const inputValueMaxLength = 254; return elementValue.length > inputValueMaxLength ? `${elementValue.substring(0, inputValueMaxLength)}...SNIPPED` : elementValue; } return elementValue; } /** * Captures the `data-*` attribute metadata to help with validating the autofill data. * * @param element - The form field element to capture the `data-*` attribute metadata from */ getDataSetValues(element) { let datasetValues = ""; const dataset = element.dataset; for (const key in dataset) { datasetValues += `${key}: ${dataset[key]}, `; } return datasetValues; } /** * Get the options from a select element and return them as an array * of arrays indicating the select element option text and value. * @param {HTMLSelectElement} element * @returns {{options: (string | null)[][]}} * @private */ getSelectElementOptions(element) { const options = Array.from(element.options).map((option) => { const optionText = option.text ? String(option.text) .toLowerCase() .replace(/[\s~`!@$%^&#*()\-_+=:;'"[\]|\\,<.>?]/gm, "") // Remove whitespace and punctuation : null; return [optionText, option.value]; }); return { options }; } /** * Queries all potential form and field elements from the DOM and returns * a collection of form and field elements. Leverages the TreeWalker API * to deep query Shadow DOM elements. */ queryAutofillFormAndFieldElements() { const formElements = []; const formFieldElements = []; const queriedElements = this.domQueryService.query(globalThis.document.documentElement, `form, ${this.formFieldQueryString}`, (node) => { if (nodeIsFormElement(node)) { formElements.push(node); return true; } if (this.isNodeFormFieldElement(node)) { formFieldElements.push(node); return true; } return false; }, this.mutationObserver); if (formElements.length || formFieldElements.length) { return { formElements, formFieldElements }; } for (let index = 0; index < queriedElements.length; index++) { const element = queriedElements[index]; if (elementIsFormElement(element)) { formElements.push(element); continue; } if (this.isNodeFormFieldElement(element)) { formFieldElements.push(element); } } return { formElements, formFieldElements }; } /** * Checks if the passed node is a form field element. * @param {Node} node * @returns {boolean} * @private */ isNodeFormFieldElement(node) { if (!nodeIsElement(node)) { return false; } const nodeTagName = node.tagName.toLowerCase(); const nodeIsSpanElementWithAutofillAttribute = nodeTagName === "span" && node.hasAttribute("data-bwautofill"); if (nodeIsSpanElementWithAutofillAttribute) { return true; } const nodeHasBwIgnoreAttribute = node.hasAttribute("data-bwignore"); const nodeIsValidInputElement = nodeTagName === "input" && !this.ignoredInputTypes.has(node.type); if (nodeIsValidInputElement && !nodeHasBwIgnoreAttribute) { return true; } return this.nonInputFormFieldTags.has(nodeTagName) && !nodeHasBwIgnoreAttribute; } /** * Sets up a mutation observer on the body of the document. Observes changes to * DOM elements to ensure we have an updated set of autofill field data. * @private */ setupMutationObserver() { this.currentLocationHref = globalThis.location.href; this.mutationObserver = new MutationObserver(this.handleMutationObserverMutation); this.mutationObserver.observe(document.documentElement, { attributes: true, childList: true, subtree: true, }); } /** * Handles a mutation to the window location. Clears the autofill elements * and updates the autofill elements after a timeout. * @private */ handleWindowLocationMutation() { this.currentLocationHref = globalThis.location.href; this.domRecentlyMutated = true; if (this.autofillOverlayContentService) { this.autofillOverlayContentService.pageDetailsUpdateRequired = true; this.autofillOverlayContentService.clearUserFilledFields(); void this.sendExtensionMessage("closeAutofillInlineMenu", { forceCloseInlineMenu: true }); } this.noFieldsFound = false; this._autofillFormElements.clear(); this.autofillFieldElements.clear(); this.updateAutofillElementsAfterMutation(); } /** * Triggers several flags that indicate that a collection of page details should * occur again on a subsequent call after a mutation has been observed in the DOM. */ flagPageDetailsUpdateIsRequired() { this.domRecentlyMutated = true; if (this.autofillOverlayContentService) { this.autofillOverlayContentService.pageDetailsUpdateRequired = true; } this.noFieldsFound = false; } /** * Processes all mutation records encountered by the mutation observer. * * @param mutations - The mutation record to process */ processMutationRecords(mutations) { for (let mutationIndex = 0; mutationIndex < mutations.length; mutationIndex++) { const mutation = mutations[mutationIndex]; const processMutationRecord = () => this.processMutationRecord(mutation); requestIdleCallbackPolyfill(processMutationRecord, { timeout: 500 }); } } /** * Processes a single mutation record and updates the autofill elements if necessary. * @param mutation * @private */ processMutationRecord(mutation) { this.handleTopLayerChanges(mutation); if (mutation.type === "childList" && (this.isAutofillElementNodeMutated(mutation.removedNodes, true) || this.isAutofillElementNodeMutated(mutation.addedNodes))) { this.flagPageDetailsUpdateIsRequired(); return; } if (mutation.type === "attributes") { this.handleAutofillElementAttributeMutation(mutation); } } /** * Checks if the passed nodes either contain or are autofill elements. * * @param nodes - The nodes to check * @param isRemovingNodes - Whether the nodes are being removed */ isAutofillElementNodeMutated(nodes, isRemovingNodes = false) { if (!nodes.length) { return false; } let isElementMutated = false; let mutatedElements = []; for (let index = 0; index < nodes.length; index++) { const node = nodes[index]; if (!nodeIsElement(node)) { continue; } if (nodeIsFormElement(node) || this.isNodeFormFieldElement(node)) { mutatedElements.push(node); } const autofillElements = this.domQueryService.query(node, `form, ${this.formFieldQueryString}`, (walkerNode) => nodeIsFormElement(walkerNode) || this.isNodeFormFieldElement(walkerNode), this.mutationObserver, true); if (autofillElements.length) { mutatedElements = mutatedElements.concat(autofillElements); } if (mutatedElements.length) { isElementMutated = true; } } if (isRemovingNodes) { for (let elementIndex = 0; elementIndex < mutatedElements.length; elementIndex++) { const element = mutatedElements[elementIndex]; this.deleteCachedAutofillElement(element); } } else if (this.autofillOverlayContentService) { this.setupOverlayListenersOnMutatedElements(mutatedElements); } return isElementMutated; } /** * Sets up the overlay listeners on the passed mutated elements. This ensures * that the overlay can appear on elements that are injected into the DOM after * the initial page load. * * @param mutatedElements - HTML elements that have been mutated */ setupOverlayListenersOnMutatedElements(mutatedElements) { for (let elementIndex = 0; elementIndex < mutatedElements.length; elementIndex++) { const node = mutatedElements[elementIndex]; const buildAutofillFieldItem = () => collect_autofill_content_service_awaiter(this, void 0, void 0, function* () { if (!this.isNodeFormFieldElement(node) || this.autofillFieldElements.get(node)) { return; } // We are setting this item to a -1 index because we do not know its position in the DOM. // This value should be updated with the next call to collect page details. const formFieldElement = node; const autofillField = yield this.buildAutofillFieldItem(formFieldElement, -1); // Set up overlay listeners for the new field if we have the overlay service if (autofillField && this.autofillOverlayContentService) { this.setupOverlayOnField(formFieldElement, autofillField); if (this.domRecentlyMutated) { this.updateAutofillElementsAfterMutation(); } } }); requestIdleCallbackPolyfill(buildAutofillFieldItem, { timeout: 1000 }); } } /** * Deletes any cached autofill elements that have been * removed from the DOM. * @param {ElementWithOpId | ElementWithOpId} element * @private */ deleteCachedAutofillElement(element) { if (elementIsFormElement(element) && this._autofillFormElements.has(element)) { this._autofillFormElements.delete(element); return; } if (this.autofillFieldElements.has(element)) { this.autofillFieldElements.delete(element); } } /** * Updates the autofill elements after a DOM mutation has occurred. * Is debounced to prevent excessive updates. * @private */ updateAutofillElementsAfterMutation() { if (this.updateAfterMutationIdleCallback) { cancelIdleCallbackPolyfill(this.updateAfterMutationIdleCallback); } this.updateAfterMutationIdleCallback = requestIdleCallbackPolyfill(this.getPageDetails.bind(this), { timeout: this.updateAfterMutationTimeout }); } /** * Handles observed DOM mutations related to an autofill element attribute. * @param {MutationRecord} mutation * @private */ handleAutofillElementAttributeMutation(mutation) { var _a; const targetElement = mutation.target; if (!nodeIsElement(targetElement)) { return; } const attributeName = (_a = mutation.attributeName) === null || _a === void 0 ? void 0 : _a.toLowerCase(); const autofillForm = this._autofillFormElements.get(targetElement); if (autofillForm) { this.updateAutofillFormElementData(attributeName, targetElement, autofillForm); return; } const autofillField = this.autofillFieldElements.get(targetElement); if (!autofillField) { return; } this.updateAutofillFieldElementData(attributeName, targetElement, autofillField); } /** * Updates the autofill form element data based on the passed attribute name. * @param {string} attributeName * @param {ElementWithOpId} element * @param {AutofillForm} dataTarget * @private */ updateAutofillFormElementData(attributeName, element, dataTarget) { const updateAttribute = (dataTargetKey) => { this.updateAutofillDataAttribute({ element, attributeName, dataTarget, dataTargetKey }); }; const updateActions = { action: () => (dataTarget.htmlAction = this.getFormActionAttribute(element)), name: () => updateAttribute("htmlName"), id: () => updateAttribute("htmlID"), method: () => updateAttribute("htmlMethod"), }; if (!updateActions[attributeName]) { return; } updateActions[attributeName](); if (this._autofillFormElements.has(element)) { this._autofillFormElements.set(element, dataTarget); } } /** * Updates the autofill field element data based on the passed attribute name. * * @param {string} attributeName * @param {ElementWithOpId} element * @param {AutofillField} dataTarget */ updateAutofillFieldElementData(attributeName, element, dataTarget) { const updateAttribute = (dataTargetKey) => { this.updateAutofillDataAttribute({ element, attributeName, dataTarget, dataTargetKey }); }; const updateActions = { 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-label": () => updateAttribute("label-aria"), "aria-hidden": () => (dataTarget["aria-hidden"] = this.getAttributeBoolean(element, "aria-hidden", true)), "aria-disabled": () => (dataTarget["aria-disabled"] = this.getAttributeBoolean(element, "aria-disabled", true)), "aria-haspopup": () => (dataTarget["aria-haspopup"] = this.getAttributeBoolean(element, "aria-haspopup", true)), "data-stripe": () => updateAttribute("data-stripe"), }; if (!updateActions[attributeName]) { return; } updateActions[attributeName](); if (this.autofillFieldElements.has(element)) { this.autofillFieldElements.set(element, dataTarget); } } /** * Gets the attribute value for the passed element, and returns it. If the dataTarget * and dataTargetKey are passed, it will set the value of the dataTarget[dataTargetKey]. * @param UpdateAutofillDataAttributeParams * @returns {string} * @private */ updateAutofillDataAttribute({ element, attributeName, dataTarget, dataTargetKey, }) { const attributeValue = this.getPropertyOrAttribute(element, attributeName); if (dataTarget && dataTargetKey) { dataTarget[dataTargetKey] = attributeValue; } return attributeValue; } /** * Sets up an IntersectionObserver to observe found form * field elements that are not viewable in the viewport. */ setupIntersectionObserver() { this.intersectionObserver = new IntersectionObserver(this.handleFormElementIntersection, { root: null, rootMargin: "0px", threshold: 0.9999, // Safari doesn't seem to function properly with a threshold of 1, }); } /** * Iterates over all cached field elements and sets up the inline menu listeners on each field. * * @param pageDetails - The page details to use for the inline menu listeners */ setupOverlayListeners(pageDetails) { if (this.autofillOverlayContentService) { this.autofillFieldElements.forEach((autofillField, formFieldElement) => { this.setupOverlayOnField(formFieldElement, autofillField, pageDetails); }); } } /** * Sets up the inline menu listener on the passed field element. * * @param formFieldElement - The form field element to set up the inline menu listener on * @param autofillField - The metadata for the form field * @param pageDetails - The page details to use for the inline menu listeners */ setupOverlayOnField(formFieldElement, autofillField, pageDetails) { if (this.autofillOverlayContentService) { const autofillPageDetails = pageDetails || this.getFormattedPageDetails(this.getFormattedAutofillFormsData(), this.getFormattedAutofillFieldsData()); void this.autofillOverlayContentService.setupOverlayListeners(formFieldElement, autofillField, autofillPageDetails); } } /** * Validates whether a password field is within the document. */ isPasswordFieldWithinDocument() { var _a; return (((_a = this.domQueryService.query(globalThis.document.documentElement, `input[type="password"]`, (node) => nodeIsInputElement(node) && node.type === "password")) === null || _a === void 0 ? void 0 : _a.length) > 0); } /** * Destroys the CollectAutofillContentService. Clears all * timeouts and disconnects the mutation observer. */ destroy() { var _a, _b; if (this.updateAfterMutationIdleCallback) { cancelIdleCallbackPolyfill(this.updateAfterMutationIdleCallback); } (_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect(); (_b = this.intersectionObserver) === null || _b === void 0 ? void 0 : _b.disconnect(); } } ;// ./src/autofill/models/autofill-script.ts const FillScriptActionTypes = { fill_by_opid: "fill_by_opid", click_on_opid: "click_on_opid", focus_by_opid: "focus_by_opid", }; class AutofillScript { constructor() { this.script = []; this.properties = {}; } } ;// ./src/autofill/services/insert-autofill-content.service.ts var insert_autofill_content_service_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; class InsertAutofillContentService { /** * InsertAutofillContentService constructor. Instantiates the * DomElementVisibilityService and CollectAutofillContentService classes. */ constructor(domElementVisibilityService, collectAutofillContentService) { this.domElementVisibilityService = domElementVisibilityService; this.collectAutofillContentService = collectAutofillContentService; this.autofillInsertActions = { fill_by_opid: ({ opid, value }) => this.handleFillFieldByOpidAction(opid, value), click_on_opid: ({ opid }) => this.handleClickOnFieldByOpidAction(opid), focus_by_opid: ({ opid }) => this.handleFocusOnFieldByOpidAction(opid), }; /** * Runs the autofill action based on the action type and the opid. * Each action is subsequently delayed by 20 milliseconds. * @param {FillScript} [action, opid, value] * @returns {Promise} * @private */ this.runFillScriptAction = ([action, opid, value]) => { if (!opid || !this.autofillInsertActions[action]) { return Promise.resolve(); } const delayActionsInMilliseconds = 20; return new Promise((resolve) => setTimeout(() => { if (action === FillScriptActionTypes.fill_by_opid && !!(value === null || value === void 0 ? void 0 : value.length)) { this.autofillInsertActions.fill_by_opid({ opid, value }); } else if (action === FillScriptActionTypes.click_on_opid) { this.autofillInsertActions.click_on_opid({ opid }); } else if (action === FillScriptActionTypes.focus_by_opid) { this.autofillInsertActions.focus_by_opid({ opid }); } resolve(); }, delayActionsInMilliseconds)); }; } /** * Handles autofill of the forms on the current page based on the * data within the passed fill script object. * @param {AutofillScript} fillScript * @returns {Promise} * @public */ fillForm(fillScript) { return insert_autofill_content_service_awaiter(this, void 0, void 0, function* () { var _a; if (!((_a = fillScript.script) === null || _a === void 0 ? void 0 : _a.length) || currentlyInSandboxedIframe() || this.userCancelledInsecureUrlAutofill(fillScript.savedUrls) || this.userCancelledUntrustedIframeAutofill(fillScript)) { return; } for (let index = 0; index < fillScript.script.length; index++) { yield this.runFillScriptAction(fillScript.script[index]); } }); } /** * Checks if the autofill is occurring on a page that can be considered secure. If the page is not secure, * the user is prompted to confirm that they want to autofill on the page. * @param {string[] | null} savedUrls * @returns {boolean} * @private */ userCancelledInsecureUrlAutofill(savedUrls) { if (!(savedUrls === null || savedUrls === void 0 ? void 0 : savedUrls.some((url) => url.startsWith(`https://${globalThis.location.hostname}`))) || globalThis.location.protocol !== "http:" || !this.isPasswordFieldWithinDocument()) { return false; } const confirmationWarning = [ chrome.i18n.getMessage("insecurePageWarning"), chrome.i18n.getMessage("insecurePageWarningFillPrompt", [globalThis.location.hostname]), ].join("\n\n"); return !globalThis.confirm(confirmationWarning); } /** * Checks if there is a password field within the current document. Includes * password fields that are present within the shadow DOM. * @returns {boolean} * @private */ isPasswordFieldWithinDocument() { return this.collectAutofillContentService.isPasswordFieldWithinDocument(); } /** * Checking if the autofill is occurring within an untrusted iframe. If the page is within an untrusted iframe, * the user is prompted to confirm that they want to autofill on the page. If the user cancels the autofill, * the script will not continue. * * Note: confirm() is blocked by sandboxed iframes, but we don't want to fill sandboxed iframes anyway. * If this occurs, confirm() returns false without displaying the dialog box, and autofill will be aborted. * The browser may print a message to the console, but this is not a standard error that we can handle. * @param {AutofillScript} fillScript * @returns {boolean} * @private */ userCancelledUntrustedIframeAutofill(fillScript) { if (!fillScript.untrustedIframe) { return false; } const confirmationWarning = [ chrome.i18n.getMessage("autofillIframeWarning"), chrome.i18n.getMessage("autofillIframeWarningTip", [globalThis.location.hostname]), ].join("\n\n"); return !globalThis.confirm(confirmationWarning); } /** * Queries the DOM for an element by opid and inserts the passed value into the element. * @param {string} opid * @param {string} value * @private */ handleFillFieldByOpidAction(opid, value) { const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid); this.insertValueIntoField(element, value); } /** * Handles finding an element by opid and triggering a click event on the element. * @param {string} opid * @private */ handleClickOnFieldByOpidAction(opid) { const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid); if (element) { this.triggerClickOnElement(element); } } /** * Handles finding an element by opid 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 {string} opid - The opid of the element to focus on. */ handleFocusOnFieldByOpidAction(opid) { const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid); if (!element) { return; } if (document.activeElement === element) { element.blur(); } this.simulateUserMouseClickAndFocusEventInteractions(element, true); } /** * 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 * properly updated. * @param {FormFieldElement | null} element * @param {string} value * @private */ insertValueIntoField(element, value) { if (!element || !value) { return; } const elementCanBeReadonly = elementIsInputElement(element) || elementIsTextAreaElement(element); const elementCanBeFilled = elementCanBeReadonly || elementIsSelectElement(element); const elementValue = (element === null || element === void 0 ? void 0 : element.value) || (element === null || element === void 0 ? void 0 : element.innerText) || ""; const elementAlreadyHasTheValue = !!((elementValue === null || elementValue === void 0 ? void 0 : elementValue.length) && elementValue === value); if (elementAlreadyHasTheValue || (elementCanBeReadonly && element.readOnly) || (elementCanBeFilled && element.disabled)) { return; } if (!elementIsFillableFormField(element)) { this.handleInsertValueAndTriggerSimulatedEvents(element, () => (element.innerText = value)); return; } const isFillableCheckboxOrRadioElement = elementIsInputElement(element) && new Set(["checkbox", "radio"]).has(element.type) && new Set(["true", "y", "1", "yes", "✓"]).has(String(value).toLowerCase()); if (isFillableCheckboxOrRadioElement) { this.handleInsertValueAndTriggerSimulatedEvents(element, () => (element.checked = true)); return; } this.handleInsertValueAndTriggerSimulatedEvents(element, () => (element.value = value)); } /** * Simulates pre- and post-insert events on the element meant to mimic user interactions * while inserting the autofill value into the element. * @param {FormFieldElement} element * @param {Function} valueChangeCallback * @private */ handleInsertValueAndTriggerSimulatedEvents(element, valueChangeCallback) { this.triggerPreInsertEventsOnElement(element); valueChangeCallback(); this.triggerPostInsertEventsOnElement(element); this.triggerFillAnimationOnElement(element); } /** * Simulates a mouse click event on the element, including focusing the event, and * the triggers a simulated keyboard event on the element. Will attempt to ensure * that the initial element value is not arbitrarily changed by the simulated events. * @param {FormFieldElement} element * @private */ triggerPreInsertEventsOnElement(element) { const initialElementValue = "value" in element ? element.value : ""; this.simulateUserMouseClickAndFocusEventInteractions(element); this.simulateUserKeyboardEventInteractions(element); if ("value" in element && initialElementValue !== element.value) { element.value = initialElementValue; } } /** * Simulates a keyboard event on the element before assigning the autofilled value to the element, and then * simulates an input change event on the element to trigger expected events after autofill occurs. * @param {FormFieldElement} element * @private */ triggerPostInsertEventsOnElement(element) { const autofilledValue = "value" in element ? element.value : ""; this.simulateUserKeyboardEventInteractions(element); if ("value" in element && autofilledValue !== element.value) { element.value = autofilledValue; } this.simulateInputElementChangedEvent(element); } /** * Identifies if a passed element can be animated and sets a class on the element * to trigger a CSS animation. The animation is removed after a short delay. * @param {FormFieldElement} element * @private */ triggerFillAnimationOnElement(element) { const skipAnimatingElement = elementIsFillableFormField(element) && !new Set(["email", "text", "password", "number", "tel", "url"]).has(element === null || element === void 0 ? void 0 : element.type); if (this.domElementVisibilityService.isElementHiddenByCss(element) || skipAnimatingElement) { return; } element.classList.add("com-bitwarden-browser-animated-fill"); setTimeout(() => element.classList.remove("com-bitwarden-browser-animated-fill"), 200); } /** * Simulates a click event on the element. * @param {HTMLElement} element * @private */ triggerClickOnElement(element) { if (!element || typeof element.click !== TYPE_CHECK.FUNCTION) { return; } element.click(); } /** * Simulates a focus event on the element. Will optionally reset the value of the element * if the element has a value property. * @param {HTMLElement | undefined} element * @param {boolean} shouldResetValue * @private */ triggerFocusOnElement(element, shouldResetValue = false) { if (!element || typeof element.focus !== TYPE_CHECK.FUNCTION) { return; } let initialValue = ""; if (shouldResetValue && "value" in element) { initialValue = String(element.value); } element.focus(); if (initialValue && "value" in element) { element.value = initialValue; } } /** * Simulates a mouse click and focus event on the element. * @param {FormFieldElement} element * @param {boolean} shouldResetValue * @private */ simulateUserMouseClickAndFocusEventInteractions(element, shouldResetValue = false) { this.triggerClickOnElement(element); this.triggerFocusOnElement(element, shouldResetValue); } /** * Simulates several keyboard events on the element, mocking a user interaction with the element. * @param {FormFieldElement} element * @private */ simulateUserKeyboardEventInteractions(element) { const simulatedKeyboardEvents = [EVENTS.KEYDOWN, EVENTS.KEYUP]; for (let index = 0; index < simulatedKeyboardEvents.length; index++) { element.dispatchEvent(new KeyboardEvent(simulatedKeyboardEvents[index], { bubbles: true })); } } /** * Simulates an input change event on the element, mocking behavior that would occur if a user * manually changed a value for the element. * @param {FormFieldElement} element * @private */ simulateInputElementChangedEvent(element) { const simulatedInputEvents = [EVENTS.INPUT, EVENTS.CHANGE]; for (let index = 0; index < simulatedInputEvents.length; index++) { element.dispatchEvent(new Event(simulatedInputEvents[index], { bubbles: true })); } } } /* harmony default export */ var insert_autofill_content_service = (InsertAutofillContentService); ;// ./src/autofill/content/autofill-init.ts var autofill_init_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; class AutofillInit { /** * AutofillInit constructor. Initializes the DomElementVisibilityService, * CollectAutofillContentService and InsertAutofillContentService classes. * * @param domQueryService - Service used to handle DOM queries. * @param domElementVisibilityService - Used to check if an element is viewable. * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. * @param autofillInlineMenuContentService - The inline menu content service, potentially undefined. * @param overlayNotificationsContentService - The overlay notifications content service, potentially undefined. */ constructor(domQueryService, domElementVisibilityService, autofillOverlayContentService, autofillInlineMenuContentService, overlayNotificationsContentService) { this.autofillOverlayContentService = autofillOverlayContentService; this.autofillInlineMenuContentService = autofillInlineMenuContentService; this.overlayNotificationsContentService = overlayNotificationsContentService; this.sendExtensionMessage = sendExtensionMessage; this.extensionMessageHandlers = { collectPageDetails: ({ message }) => this.collectPageDetails(message), collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true), fillForm: ({ message }) => this.fillForm(message), }; /** * Handles the extension messages sent to the content script. * * @param message - The extension message. * @param sender - The message sender. * @param sendResponse - The send response callback. */ this.handleExtensionMessage = (message, sender, sendResponse) => { const command = message.command; const handler = this.getExtensionMessageHandler(command); if (!handler) { return null; } const messageResponse = handler({ message, sender }); if (typeof messageResponse === "undefined") { return null; } void Promise.resolve(messageResponse).then((response) => sendResponse(response)); return true; }; this.collectAutofillContentService = new CollectAutofillContentService(domElementVisibilityService, domQueryService, this.autofillOverlayContentService); this.insertAutofillContentService = new insert_autofill_content_service(domElementVisibilityService, this.collectAutofillContentService); } /** * Initializes the autofill content script, setting up * the extension message listeners. This method should * be called once when the content script is loaded. */ init() { var _a; this.setupExtensionMessageListeners(); (_a = this.autofillOverlayContentService) === null || _a === void 0 ? void 0 : _a.init(); this.collectPageDetailsOnLoad(); } /** * Triggers a collection of the page details from the * background script, ensuring that autofill is ready * to act on the page. */ collectPageDetailsOnLoad() { const sendCollectDetailsMessage = () => { this.clearCollectPageDetailsOnLoadTimeout(); this.collectPageDetailsOnLoadTimeout = setTimeout(() => this.sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), 750); }; if (globalThis.document.readyState === "complete") { sendCollectDetailsMessage(); } globalThis.addEventListener(EVENTS.LOAD, sendCollectDetailsMessage); } /** * Collects the page details and sends them to the * extension background script. If the `sendDetailsInResponse` * parameter is set to true, the page details will be * returned to facilitate sending the details in the * response to the extension message. * * @param message - The extension message. * @param sendDetailsInResponse - Determines whether to send the details in the response. */ collectPageDetails(message_1) { return autofill_init_awaiter(this, arguments, void 0, function* (message, sendDetailsInResponse = false) { const pageDetails = yield this.collectAutofillContentService.getPageDetails(); if (sendDetailsInResponse) { return pageDetails; } void this.sendExtensionMessage("collectPageDetailsResponse", { tab: message.tab, details: pageDetails, sender: message.sender, }); }); } /** * Fills the form with the given fill script. * * @param {AutofillExtensionMessage} message */ fillForm(_a) { return autofill_init_awaiter(this, arguments, void 0, function* ({ fillScript, pageDetailsUrl }) { if ((document.defaultView || window).location.href !== pageDetailsUrl || !fillScript) { return; } this.blurFocusedFieldAndCloseInlineMenu(); yield this.sendExtensionMessage("updateIsFieldCurrentlyFilling", { isFieldCurrentlyFilling: true, }); yield this.insertAutofillContentService.fillForm(fillScript); setTimeout(() => this.sendExtensionMessage("updateIsFieldCurrentlyFilling", { isFieldCurrentlyFilling: false, }), 250); }); } /** * Blurs the most recently focused field and removes the inline menu. Used * in cases where the background unlock or vault item reprompt popout * is opened. */ blurFocusedFieldAndCloseInlineMenu() { var _a; (_a = this.autofillOverlayContentService) === null || _a === void 0 ? void 0 : _a.blurMostRecentlyFocusedField(true); } /** * Clears the send collect details message timeout. */ clearCollectPageDetailsOnLoadTimeout() { if (this.collectPageDetailsOnLoadTimeout) { clearTimeout(this.collectPageDetailsOnLoadTimeout); } } /** * Sets up the extension message listeners for the content script. */ setupExtensionMessageListeners() { chrome.runtime.onMessage.addListener(this.handleExtensionMessage); } /** * Gets the extension message handler for the given command. * * @param command - The extension message command. */ getExtensionMessageHandler(command) { var _a, _b, _c, _d, _e, _f; if ((_b = (_a = this.autofillOverlayContentService) === null || _a === void 0 ? void 0 : _a.messageHandlers) === null || _b === void 0 ? void 0 : _b[command]) { return this.autofillOverlayContentService.messageHandlers[command]; } if ((_d = (_c = this.autofillInlineMenuContentService) === null || _c === void 0 ? void 0 : _c.messageHandlers) === null || _d === void 0 ? void 0 : _d[command]) { return this.autofillInlineMenuContentService.messageHandlers[command]; } if ((_f = (_e = this.overlayNotificationsContentService) === null || _e === void 0 ? void 0 : _e.messageHandlers) === null || _f === void 0 ? void 0 : _f[command]) { return this.overlayNotificationsContentService.messageHandlers[command]; } return this.extensionMessageHandlers[command]; } /** * Handles destroying the autofill init content script. Removes all * listeners, timeouts, and object instances to prevent memory leaks. */ destroy() { var _a, _b, _c; this.clearCollectPageDetailsOnLoadTimeout(); chrome.runtime.onMessage.removeListener(this.handleExtensionMessage); this.collectAutofillContentService.destroy(); (_a = this.autofillOverlayContentService) === null || _a === void 0 ? void 0 : _a.destroy(); (_b = this.autofillInlineMenuContentService) === null || _b === void 0 ? void 0 : _b.destroy(); (_c = this.overlayNotificationsContentService) === null || _c === void 0 ? void 0 : _c.destroy(); } } /* harmony default export */ var autofill_init = (AutofillInit); ;// ./src/autofill/content/bootstrap-autofill.ts (function (windowContext) { if (!windowContext.bitwardenAutofillInit) { const domQueryService = new DomQueryService(); const domElementVisibilityService = new dom_element_visibility_service(); windowContext.bitwardenAutofillInit = new autofill_init(domQueryService, domElementVisibilityService); setupAutofillInitDisconnectAction(windowContext); windowContext.bitwardenAutofillInit.init(); } })(window); /******/ })() ;