bitwarden-estensione-browser/apps/browser/src/content/notificationBar.ts

604 lines
18 KiB
TypeScript

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<string>) {
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);
}
});