import AddLoginRuntimeMessage from "src/background/models/addLoginRuntimeMessage"; import ChangePasswordRuntimeMessage from "src/background/models/changePasswordRuntimeMessage"; document.addEventListener("DOMContentLoaded", (event) => { if (window.location.hostname.endsWith("vault.bitwarden.com")) { return; } const pageDetails: any[] = []; const formData: any[] = []; let barType: string = null; let pageHref: string = null; let observer: MutationObserver = null; const observeIgnoredElements = new Set([ "a", "i", "b", "strong", "span", "code", "br", "img", "small", "em", "hr", ]); let domObservationCollectTimeout: number = null; let collectIfNeededTimeout: number = null; let observeDomTimeout: number = null; const inIframe = isInIframe(); const cancelButtonNames = new Set(["cancel", "close", "back"]); const logInButtonNames = new Set([ "log in", "sign in", "login", "go", "submit", "continue", "next", ]); const changePasswordButtonNames = new Set([ "save password", "update password", "change password", "change", ]); const changePasswordButtonContainsNames = new Set(["pass", "change", "contras", "senha"]); let disabledAddLoginNotification = false; let disabledChangedPasswordNotification = false; const activeUserIdKey = "activeUserId"; let activeUserId: string; chrome.storage.local.get(activeUserIdKey, (obj: any) => { if (obj == null || obj[activeUserIdKey] == null) { return; } activeUserId = obj[activeUserIdKey]; }); chrome.storage.local.get(activeUserId, (obj: any) => { if (obj?.[activeUserId] == null) { return; } const domains = obj[activeUserId].settings.neverDomains; // eslint-disable-next-line if (domains != null && domains.hasOwnProperty(window.location.hostname)) { return; } disabledAddLoginNotification = obj[activeUserId].settings.disableAddLoginNotification; disabledChangedPasswordNotification = obj[activeUserId].settings.disableChangedPasswordNotification; if (!disabledAddLoginNotification || !disabledChangedPasswordNotification) { collectIfNeededWithTimeout(); } }); chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { processMessages(msg, sendResponse); }); function processMessages(msg: any, sendResponse: (response?: any) => void) { if (msg.command === "openNotificationBar") { if (inIframe) { return; } closeExistingAndOpenBar(msg.data.type, msg.data.typeData); sendResponse(); return true; } else if (msg.command === "closeNotificationBar") { if (inIframe) { return; } closeBar(true); sendResponse(); return true; } else if (msg.command === "adjustNotificationBar") { if (inIframe) { return; } adjustBar(msg.data); sendResponse(); return true; } else if (msg.command === "notificationBarPageDetails") { pageDetails.push(msg.data.details); watchForms(msg.data.forms); sendResponse(); return true; } } function isInIframe() { try { return window.self !== window.top; } catch { return true; } } function observeDom() { const bodies = document.querySelectorAll("body"); if (bodies && bodies.length > 0) { observer = new MutationObserver((mutations) => { if (mutations == null || mutations.length === 0 || pageHref !== window.location.href) { return; } let doCollect = false; for (let i = 0; i < mutations.length; i++) { const mutation = mutations[i]; if (mutation.addedNodes == null || mutation.addedNodes.length === 0) { continue; } for (let j = 0; j < mutation.addedNodes.length; j++) { const addedNode: any = mutation.addedNodes[j]; if (addedNode == null) { continue; } const tagName = addedNode.tagName != null ? addedNode.tagName.toLowerCase() : null; if ( tagName != null && tagName === "form" && (addedNode.dataset == null || !addedNode.dataset.bitwardenWatching) ) { doCollect = true; break; } if ( (tagName != null && observeIgnoredElements.has(tagName)) || addedNode.querySelectorAll == null ) { continue; } const forms = addedNode.querySelectorAll("form:not([data-bitwarden-watching])"); if (forms != null && forms.length > 0) { doCollect = true; break; } } if (doCollect) { break; } } if (doCollect) { if (domObservationCollectTimeout != null) { window.clearTimeout(domObservationCollectTimeout); } domObservationCollectTimeout = window.setTimeout(collect, 1000); } }); observer.observe(bodies[0], { childList: true, subtree: true }); } } function collectIfNeededWithTimeout() { if (collectIfNeededTimeout != null) { window.clearTimeout(collectIfNeededTimeout); } collectIfNeededTimeout = window.setTimeout(collectIfNeeded, 1000); } function collectIfNeeded() { if (pageHref !== window.location.href) { pageHref = window.location.href; if (observer) { observer.disconnect(); observer = null; } collect(); if (observeDomTimeout != null) { window.clearTimeout(observeDomTimeout); } observeDomTimeout = window.setTimeout(observeDom, 1000); } if (collectIfNeededTimeout != null) { window.clearTimeout(collectIfNeededTimeout); } collectIfNeededTimeout = window.setTimeout(collectIfNeeded, 1000); } function collect() { sendPlatformMessage({ command: "bgCollectPageDetails", sender: "notificationBar", }); } function watchForms(forms: any[]) { if (forms == null || forms.length === 0) { return; } forms.forEach((f: any) => { const formId: string = f.form != null ? f.form.htmlID : null; let formEl: HTMLFormElement = null; if (formId != null && formId !== "") { formEl = document.getElementById(formId) as HTMLFormElement; } if (formEl == null) { const index = parseInt(f.form.opid.split("__")[2], null); formEl = document.getElementsByTagName("form")[index]; } if (formEl != null && formEl.dataset.bitwardenWatching !== "1") { const formDataObj: any = { data: f, formEl: formEl, usernameEl: null, passwordEl: null, passwordEls: null, }; locateFields(formDataObj); formData.push(formDataObj); listen(formEl); formEl.dataset.bitwardenWatching = "1"; } }); } function listen(form: HTMLFormElement) { form.removeEventListener("submit", formSubmitted, false); form.addEventListener("submit", formSubmitted, false); const submitButton = getSubmitButton(form, logInButtonNames); if (submitButton != null) { submitButton.removeEventListener("click", formSubmitted, false); submitButton.addEventListener("click", formSubmitted, false); } } function locateFields(formDataObj: any) { const inputs = Array.from(document.getElementsByTagName("input")); formDataObj.usernameEl = locateField(formDataObj.formEl, formDataObj.data.username, inputs); if (formDataObj.usernameEl != null && formDataObj.data.password != null) { formDataObj.passwordEl = locatePassword( formDataObj.formEl, formDataObj.data.password, inputs, true ); } else if (formDataObj.data.passwords != null) { formDataObj.passwordEls = []; formDataObj.data.passwords.forEach((pData: any) => { const el = locatePassword(formDataObj.formEl, pData, inputs, false); if (el != null) { formDataObj.passwordEls.push(el); } }); if (formDataObj.passwordEls.length === 0) { formDataObj.passwordEls = null; } } } function locatePassword( form: HTMLFormElement, passwordData: any, inputs: HTMLInputElement[], doLastFallback: boolean ) { let el = locateField(form, passwordData, inputs); if (el != null && el.type !== "password") { el = null; } if (doLastFallback && el == null) { el = form.querySelector('input[type="password"]'); } return el; } function locateField(form: HTMLFormElement, fieldData: any, inputs: HTMLInputElement[]) { if (fieldData == null) { return; } let el: HTMLInputElement = null; if (fieldData.htmlID != null && fieldData.htmlID !== "") { try { el = form.querySelector("#" + fieldData.htmlID); } catch { // Ignore error, we perform fallbacks below. } } if (el == null && fieldData.htmlName != null && fieldData.htmlName !== "") { el = form.querySelector('input[name="' + fieldData.htmlName + '"]'); } if (el == null && fieldData.elementNumber != null) { el = inputs[fieldData.elementNumber]; } return el; } function formSubmitted(e: Event) { let form: HTMLFormElement = null; if (e.type === "click") { form = (e.target as HTMLElement).closest("form"); if (form == null) { const parentModal = (e.target as HTMLElement).closest("div.modal"); if (parentModal != null) { const modalForms = parentModal.querySelectorAll("form"); if (modalForms.length === 1) { form = modalForms[0]; } } } } else { form = e.target as HTMLFormElement; } if (form == null || form.dataset.bitwardenProcessed === "1") { return; } for (let i = 0; i < formData.length; i++) { if (formData[i].formEl !== form) { continue; } const disabledBoth = disabledChangedPasswordNotification && disabledAddLoginNotification; if (!disabledBoth && formData[i].usernameEl != null && formData[i].passwordEl != null) { const login: AddLoginRuntimeMessage = { username: formData[i].usernameEl.value, password: formData[i].passwordEl.value, url: document.URL, }; if ( login.username != null && login.username !== "" && login.password != null && login.password !== "" ) { processedForm(form); sendPlatformMessage({ command: "bgAddLogin", login: login, }); break; } } if (!disabledChangedPasswordNotification && formData[i].passwordEls != null) { const passwords: string[] = formData[i].passwordEls .filter((el: HTMLInputElement) => el.value != null && el.value !== "") .map((el: HTMLInputElement) => el.value); let curPass: string = null; let newPass: string = null; let newPassOnly = false; if (formData[i].passwordEls.length === 3 && passwords.length === 3) { newPass = passwords[1]; if (passwords[0] !== newPass && newPass === passwords[2]) { curPass = passwords[0]; } else if (newPass !== passwords[2] && passwords[0] === newPass) { curPass = passwords[2]; } } else if (formData[i].passwordEls.length === 2 && passwords.length === 2) { if (passwords[0] === passwords[1]) { newPassOnly = true; newPass = passwords[0]; curPass = null; } else { const buttonText = getButtonText(getSubmitButton(form, changePasswordButtonNames)); const matches = Array.from(changePasswordButtonContainsNames).filter( (n) => buttonText.indexOf(n) > -1 ); if (matches.length > 0) { curPass = passwords[0]; newPass = passwords[1]; } } } if ((newPass != null && curPass != null) || (newPassOnly && newPass != null)) { processedForm(form); const changePasswordRuntimeMessage: ChangePasswordRuntimeMessage = { newPassword: newPass, currentPassword: curPass, url: document.URL, }; sendPlatformMessage({ command: "bgChangedPassword", data: changePasswordRuntimeMessage, }); break; } } } } function getSubmitButton(wrappingEl: HTMLElement, buttonNames: Set) { if (wrappingEl == null) { return null; } const wrappingElIsForm = wrappingEl.tagName.toLowerCase() === "form"; let submitButton = wrappingEl.querySelector( 'input[type="submit"], input[type="image"], ' + 'button[type="submit"]' ) as HTMLElement; if (submitButton == null && wrappingElIsForm) { submitButton = wrappingEl.querySelector("button:not([type])"); if (submitButton != null) { const buttonText = getButtonText(submitButton); if (buttonText != null && cancelButtonNames.has(buttonText.trim().toLowerCase())) { submitButton = null; } } } if (submitButton == null) { const possibleSubmitButtons = Array.from( wrappingEl.querySelectorAll( 'a, span, button[type="button"], ' + 'input[type="button"], button:not([type])' ) ) as HTMLElement[]; let typelessButton: HTMLElement = null; possibleSubmitButtons.forEach((button) => { if (submitButton != null || button == null || button.tagName == null) { return; } const buttonText = getButtonText(button); if (buttonText != null) { if ( typelessButton != null && button.tagName.toLowerCase() === "button" && button.getAttribute("type") == null && !cancelButtonNames.has(buttonText.trim().toLowerCase()) ) { typelessButton = button; } else if (buttonNames.has(buttonText.trim().toLowerCase())) { submitButton = button; } } }); if (submitButton == null && typelessButton != null) { submitButton = typelessButton; } } if (submitButton == null && wrappingElIsForm) { // Maybe it's in a modal? const parentModal = wrappingEl.closest("div.modal") as HTMLElement; if (parentModal != null) { const modalForms = parentModal.querySelectorAll("form"); if (modalForms.length === 1) { submitButton = getSubmitButton(parentModal, buttonNames); } } } return submitButton; } function getButtonText(button: HTMLElement) { let buttonText: string = null; if (button.tagName.toLowerCase() === "input") { buttonText = (button as HTMLInputElement).value; } else { buttonText = button.innerText; } return buttonText; } function processedForm(form: HTMLFormElement) { form.dataset.bitwardenProcessed = "1"; window.setTimeout(() => { form.dataset.bitwardenProcessed = "0"; }, 500); } function closeExistingAndOpenBar(type: string, typeData: any) { let barPage = "notification/bar.html"; switch (type) { case "add": barPage = barPage + "?add=1&isVaultLocked=" + typeData.isVaultLocked; break; case "change": barPage = barPage + "?change=1&isVaultLocked=" + typeData.isVaultLocked; break; default: break; } const frame = document.getElementById("bit-notification-bar-iframe") as HTMLIFrameElement; if (frame != null && frame.src.indexOf(barPage) >= 0) { return; } closeBar(false); openBar(type, barPage); } function openBar(type: string, barPage: string) { barType = type; if (document.body == null) { return; } const barPageUrl: string = chrome.extension.getURL(barPage); const iframe = document.createElement("iframe"); iframe.style.cssText = "height: 42px; width: 100%; border: 0; min-height: initial;"; iframe.id = "bit-notification-bar-iframe"; iframe.src = barPageUrl; const frameDiv = document.createElement("div"); frameDiv.setAttribute("aria-live", "polite"); frameDiv.id = "bit-notification-bar"; frameDiv.style.cssText = "height: 42px; width: 100%; top: 0; left: 0; padding: 0; position: fixed; " + "z-index: 2147483647; visibility: visible;"; frameDiv.appendChild(iframe); document.body.appendChild(frameDiv); (iframe.contentWindow.location as any) = barPageUrl; const spacer = document.createElement("div"); spacer.id = "bit-notification-bar-spacer"; spacer.style.cssText = "height: 42px;"; document.body.insertBefore(spacer, document.body.firstChild); } function closeBar(explicitClose: boolean) { const barEl = document.getElementById("bit-notification-bar"); if (barEl != null) { barEl.parentElement.removeChild(barEl); } const spacerEl = document.getElementById("bit-notification-bar-spacer"); if (spacerEl) { spacerEl.parentElement.removeChild(spacerEl); } if (!explicitClose) { return; } switch (barType) { case "add": sendPlatformMessage({ command: "bgAddClose", }); break; case "change": sendPlatformMessage({ command: "bgChangeClose", }); break; default: break; } } function adjustBar(data: any) { if (data != null && data.height !== 42) { const newHeight = data.height + "px"; doHeightAdjustment("bit-notification-bar-iframe", newHeight); doHeightAdjustment("bit-notification-bar", newHeight); doHeightAdjustment("bit-notification-bar-spacer", newHeight); } } function doHeightAdjustment(elId: string, heightStyle: string) { const el = document.getElementById(elId); if (el != null) { el.style.height = heightStyle; } } function sendPlatformMessage(msg: any) { chrome.runtime.sendMessage(msg); } });