diff --git a/jslib b/jslib index 41ab22a82fb..a1112988c46 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 41ab22a82fb5fafcce2e8d1abe86789e152ef1fa +Subproject commit a1112988c4624219461acf8269058353203969c2 diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 08ae31222e2..b8428264039 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -485,6 +485,12 @@ "notificationNeverSave": { "message": "Never for this website" }, + "notificationChangeDesc": { + "message": "Do you want to update this password in Bitwarden?" + }, + "notificationChangeSave": { + "message": "Yes, Update Now" + }, "disableContextMenuItem": { "message": "Disable Context Menu Options" }, diff --git a/src/background/main.background.ts b/src/background/main.background.ts index 92c7fa8ea32..0c6a670c5c5 100644 --- a/src/background/main.background.ts +++ b/src/background/main.background.ts @@ -95,7 +95,7 @@ export default class MainBackground { onUpdatedRan: boolean; onReplacedRan: boolean; loginToAutoFill: any = null; - loginsToAdd: any[] = []; + notificationQueue: any[] = []; private commandsBackground: CommandsBackground; private contextMenusBackground: ContextMenusBackground; @@ -195,9 +195,8 @@ export default class MainBackground { setTimeout(async () => { await this.environmentService.setUrlsFromStorage(); await this.setIcon(); - this.cleanupLoginsToAdd(); + this.cleanupNotificationQueue(); await this.fullSync(true); - resolve(); }, 500); }); @@ -284,19 +283,19 @@ export default class MainBackground { }, options); } - async checkLoginsToAdd(tab: any = null): Promise { - if (!this.loginsToAdd.length) { + async checkNotificationQueue(tab: any = null): Promise { + if (this.notificationQueue.length === 0) { return; } if (tab != null) { - this.doCheck(tab); + this.doNotificationQueueCheck(tab); return; } const currentTab = await BrowserApi.getTabFromCurrentWindow(); if (currentTab != null) { - this.doCheck(currentTab); + this.doNotificationQueueCheck(currentTab); } } @@ -499,17 +498,16 @@ export default class MainBackground { } } - private cleanupLoginsToAdd() { - for (let i = this.loginsToAdd.length - 1; i >= 0; i--) { - if (this.loginsToAdd[i].expires < new Date()) { - this.loginsToAdd.splice(i, 1); + private cleanupNotificationQueue() { + for (let i = this.notificationQueue.length - 1; i >= 0; i--) { + if (this.notificationQueue[i].expires < new Date()) { + this.notificationQueue.splice(i, 1); } } - - setTimeout(() => this.cleanupLoginsToAdd(), 2 * 60 * 1000); // check every 2 minutes + setTimeout(() => this.cleanupNotificationQueue(), 2 * 60 * 1000); // check every 2 minutes } - private doCheck(tab: any) { + private doNotificationQueueCheck(tab: any) { if (tab == null) { return; } @@ -519,14 +517,19 @@ export default class MainBackground { return; } - for (let i = 0; i < this.loginsToAdd.length; i++) { - if (this.loginsToAdd[i].tabId !== tab.id || this.loginsToAdd[i].domain !== tabDomain) { + for (let i = 0; i < this.notificationQueue.length; i++) { + if (this.notificationQueue[i].tabId !== tab.id || this.notificationQueue[i].domain !== tabDomain) { continue; } - - BrowserApi.tabSendMessageData(tab, 'openNotificationBar', { - type: 'add', - }); + if (this.notificationQueue[i].type === 'addLogin') { + BrowserApi.tabSendMessageData(tab, 'openNotificationBar', { + type: 'add', + }); + } else if (this.notificationQueue[i].type === 'changePassword') { + BrowserApi.tabSendMessageData(tab, 'openNotificationBar', { + type: 'change', + }); + } break; } } diff --git a/src/background/runtime.background.ts b/src/background/runtime.background.ts index e029f254052..67e27f390c9 100644 --- a/src/background/runtime.background.ts +++ b/src/background/runtime.background.ts @@ -114,12 +114,19 @@ export default class RuntimeBackground { case 'bgAddLogin': await this.addLogin(msg.login, sender.tab); break; + case 'bgChangedPassword': + await this.changedPassword(msg.data, sender.tab); + break; case 'bgAddClose': - this.removeAddLogin(sender.tab); + case 'bgChangeClose': + this.removeTabFromNotificationQueue(sender.tab); break; case 'bgAddSave': await this.saveAddLogin(sender.tab); break; + case 'bgChangeSave': + await this.saveChangePassword(sender.tab); + break; case 'bgNeverSave': await this.saveNever(sender.tab); break; @@ -181,27 +188,27 @@ export default class RuntimeBackground { } private async saveAddLogin(tab: any) { - for (let i = this.main.loginsToAdd.length - 1; i >= 0; i--) { - if (this.main.loginsToAdd[i].tabId !== tab.id) { + for (let i = this.main.notificationQueue.length - 1; i >= 0; i--) { + const queueMessage = this.main.notificationQueue[i]; + if (queueMessage.tabId !== tab.id || queueMessage.type !== 'addLogin') { continue; } - const loginInfo = this.main.loginsToAdd[i]; const tabDomain = this.platformUtilsService.getDomain(tab.url); - if (tabDomain != null && tabDomain !== loginInfo.domain) { + if (tabDomain != null && tabDomain !== queueMessage.domain) { continue; } - this.main.loginsToAdd.splice(i, 1); + this.main.notificationQueue.splice(i, 1); const loginModel = new LoginView(); const loginUri = new LoginUriView(); - loginUri.uri = loginInfo.uri; + loginUri.uri = queueMessage.uri; loginModel.uris = [loginUri]; - loginModel.username = loginInfo.username; - loginModel.password = loginInfo.password; + loginModel.username = queueMessage.username; + loginModel.password = queueMessage.password; const model = new CipherView(); - model.name = Utils.getHostname(loginInfo.uri) || loginInfo.domain; + model.name = Utils.getHostname(queueMessage.uri) || queueMessage.domain; model.type = CipherType.Login; model.login = loginModel; @@ -216,19 +223,49 @@ export default class RuntimeBackground { } } - private async saveNever(tab: any) { - for (let i = this.main.loginsToAdd.length - 1; i >= 0; i--) { - if (this.main.loginsToAdd[i].tabId !== tab.id) { + private async saveChangePassword(tab: any) { + for (let i = this.main.notificationQueue.length - 1; i >= 0; i--) { + const queueMessage = this.main.notificationQueue[i]; + if (queueMessage.tabId !== tab.id || queueMessage.type !== 'changePassword') { continue; } - const loginInfo = this.main.loginsToAdd[i]; const tabDomain = this.platformUtilsService.getDomain(tab.url); - if (tabDomain != null && tabDomain !== loginInfo.domain) { + if (tabDomain != null && tabDomain !== queueMessage.domain) { continue; } - this.main.loginsToAdd.splice(i, 1); + this.main.notificationQueue.splice(i, 1); + + const cipher = await this.cipherService.get(queueMessage.cipherId); + if (cipher != null && cipher.type === CipherType.Login) { + const model = await cipher.decrypt(); + model.login.password = queueMessage.newPassword; + const newCipher = await this.cipherService.encrypt(model); + await this.cipherService.saveWithServer(newCipher); + this.analytics.ga('send', { + hitType: 'event', + eventAction: 'Changed Password from Notification Bar', + }); + } + + BrowserApi.tabSendMessageData(tab, 'closeNotificationBar'); + } + } + + private async saveNever(tab: any) { + for (let i = this.main.notificationQueue.length - 1; i >= 0; i--) { + const queueMessage = this.main.notificationQueue[i]; + if (queueMessage.tabId !== tab.id || queueMessage.type !== 'addLogin') { + continue; + } + + const tabDomain = this.platformUtilsService.getDomain(tab.url); + if (tabDomain != null && tabDomain !== queueMessage.domain) { + continue; + } + + this.main.notificationQueue.splice(i, 1); const hostname = Utils.getHostname(tab.url); await this.cipherService.saveNeverDomain(hostname); BrowserApi.tabSendMessageData(tab, 'closeNotificationBar'); @@ -251,10 +288,10 @@ export default class RuntimeBackground { } if (!match) { - // remove any old logins for this tab - this.removeAddLogin(tab); - - this.main.loginsToAdd.push({ + // remove any old messages for this tab + this.removeTabFromNotificationQueue(tab); + this.main.notificationQueue.push({ + type: 'addLogin', username: loginInfo.username, password: loginInfo.password, domain: loginDomain, @@ -262,15 +299,37 @@ export default class RuntimeBackground { tabId: tab.id, expires: new Date((new Date()).getTime() + 30 * 60000), // 30 minutes }); - - await this.main.checkLoginsToAdd(tab); + await this.main.checkNotificationQueue(tab); } } - private removeAddLogin(tab: any) { - for (let i = this.main.loginsToAdd.length - 1; i >= 0; i--) { - if (this.main.loginsToAdd[i].tabId === tab.id) { - this.main.loginsToAdd.splice(i, 1); + private async changedPassword(changeData: any, tab: any) { + const loginDomain = this.platformUtilsService.getDomain(changeData.url); + if (loginDomain == null) { + return; + } + + const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url); + const matches = ciphers.filter((c) => c.login.password === changeData.currentPassword); + if (matches.length === 1) { + // remove any old messages for this tab + this.removeTabFromNotificationQueue(tab); + this.main.notificationQueue.push({ + type: 'changePassword', + cipherId: matches[0].id, + newPassword: changeData.newPassword, + domain: loginDomain, + tabId: tab.id, + expires: new Date((new Date()).getTime() + 30 * 60000), // 30 minutes + }); + await this.main.checkNotificationQueue(tab); + } + } + + private removeTabFromNotificationQueue(tab: any) { + for (let i = this.main.notificationQueue.length - 1; i >= 0; i--) { + if (this.main.notificationQueue[i].tabId === tab.id) { + this.main.notificationQueue.splice(i, 1); } } } @@ -373,6 +432,8 @@ export default class RuntimeBackground { notificationAddSave: this.i18nService.t('notificationAddSave'), notificationNeverSave: this.i18nService.t('notificationNeverSave'), notificationAddDesc: this.i18nService.t('notificationAddDesc'), + notificationChangeSave: this.i18nService.t('notificationChangeSave'), + notificationChangeDesc: this.i18nService.t('notificationChangeDesc'), }; } diff --git a/src/background/tabs.background.ts b/src/background/tabs.background.ts index 9daf12a5280..b6764d8dd3b 100644 --- a/src/background/tabs.background.ts +++ b/src/background/tabs.background.ts @@ -22,7 +22,7 @@ export default class TabsBackground { }, true); this.tabs.addEventListener('navigate', async (ev: any) => { - await this.main.checkLoginsToAdd(); + await this.main.checkNotificationQueue(); await this.main.refreshBadgeAndMenu(); }, true); @@ -38,7 +38,7 @@ export default class TabsBackground { return; } this.main.onReplacedRan = true; - await this.main.checkLoginsToAdd(); + await this.main.checkNotificationQueue(); await this.main.refreshBadgeAndMenu(); }); @@ -47,7 +47,7 @@ export default class TabsBackground { return; } this.main.onUpdatedRan = true; - await this.main.checkLoginsToAdd(); + await this.main.checkNotificationQueue(); await this.main.refreshBadgeAndMenu(); }); } diff --git a/src/content/notificationBar.ts b/src/content/notificationBar.ts index 2967452c124..4160561e97f 100644 --- a/src/content/notificationBar.ts +++ b/src/content/notificationBar.ts @@ -222,6 +222,7 @@ document.addEventListener('DOMContentLoaded', (event) => { formEl: formEl, usernameEl: null, passwordEl: null, + passwordEls: null, }; locateFields(formDataObj); formData.push(formDataObj); @@ -240,8 +241,8 @@ document.addEventListener('DOMContentLoaded', (event) => { submitButton.removeEventListener('click', formSubmitted, false); submitButton.addEventListener('click', formSubmitted, false); } else { - const possibleSubmitButtons = form.querySelectorAll('a, span, button[type="button"], ' + - 'input[type="button"]') as NodeListOf; + const possibleSubmitButtons = Array.from(form.querySelectorAll('a, span, button[type="button"], ' + + 'input[type="button"]')) as HTMLElement[]; possibleSubmitButtons.forEach((button) => { if (button == null || button.tagName == null) { return; @@ -268,41 +269,53 @@ document.addEventListener('DOMContentLoaded', (event) => { } function locateFields(formDataObj: any) { - const passwordId: string = formDataObj.data.password != null ? formDataObj.data.password.htmlID : null; - const usernameId: string = formDataObj.data.username != null ? formDataObj.data.username.htmlID : null; - const passwordName: string = formDataObj.data.password != null ? formDataObj.data.password.htmlName : null; - const usernameName: string = formDataObj.data.username != null ? formDataObj.data.username.htmlName : null; - const inputs = document.getElementsByTagName('input'); - - if (passwordId != null && passwordId !== '') { - try { - formDataObj.passwordEl = formDataObj.formEl.querySelector('#' + passwordId); - } catch { } - } - if (formDataObj.passwordEl == null && passwordName !== '') { - formDataObj.passwordEl = formDataObj.formEl.querySelector('input[name="' + passwordName + '"]'); - } - if (formDataObj.passwordEl == null && formDataObj.passwordEl != null) { - formDataObj.passwordEl = inputs[formDataObj.data.password.elementNumber]; - if (formDataObj.passwordEl != null && formDataObj.passwordEl.type !== 'password') { - formDataObj.passwordEl = null; + 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.data.passwords.length === 3) { + 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 !== 3) { + formDataObj.passwordEls = null; } } - if (formDataObj.passwordEl == null) { - formDataObj.passwordEl = formDataObj.formEl.querySelector('input[type="password"]'); - } + } - if (usernameId != null && usernameId !== '') { + 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 { - formDataObj.usernameEl = formDataObj.formEl.querySelector('#' + usernameId); + el = form.querySelector('#' + fieldData.htmlID); } catch { } } - if (formDataObj.usernameEl == null && usernameName !== '') { - formDataObj.usernameEl = formDataObj.formEl.querySelector('input[name="' + usernameName + '"]'); + if (el == null && fieldData.htmlName != null && fieldData.htmlName !== '') { + el = form.querySelector('input[name="' + fieldData.htmlName + '"]'); } - if (formDataObj.usernameEl == null && formDataObj.data.username != null) { - formDataObj.usernameEl = inputs[formDataObj.data.username.elementNumber]; + if (el == null && fieldData.elementNumber != null) { + el = inputs[fieldData.elementNumber]; } + return el; } function formSubmitted(e: Event) { @@ -321,31 +334,59 @@ document.addEventListener('DOMContentLoaded', (event) => { if (formData[i].formEl !== form) { continue; } - if (formData[i].usernameEl == null || formData[i].passwordEl == null) { - break; + if (formData[i].usernameEl != null && formData[i].passwordEl != null) { + const login = { + 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; + } } - - const login = { - username: formData[i].usernameEl.value, - password: formData[i].passwordEl.value, - url: document.URL, - }; - - if (login.username != null && login.username !== '' && login.password != null && login.password !== '') { - form.dataset.bitwardenProcessed = '1'; - window.setTimeout(() => { - form.dataset.bitwardenProcessed = '0'; - }, 500); - - sendPlatformMessage({ - command: 'bgAddLogin', - login: login, - }); - break; + if (formData[i].passwordEls != null && formData[i].passwordEls.length === 3) { + const passwords = formData[i].passwordEls + .filter((el: HTMLInputElement) => el.value != null && el.value !== '') + .map((el: HTMLInputElement) => el.value); + if (passwords.length === 3) { + const newPass: string = passwords[1]; + let curPass: string = null; + if (passwords[0] !== newPass && newPass === passwords[2]) { + curPass = passwords[0]; + } else if (newPass !== passwords[2] && passwords[0] === newPass) { + curPass = passwords[2]; + } + if (newPass != null && curPass != null) { + processedForm(form); + sendPlatformMessage({ + command: 'bgChangedPassword', + data: { + newPassword: newPass, + currentPassword: curPass, + url: document.URL, + }, + }); + break; + } + } } } } + 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) { @@ -364,6 +405,9 @@ document.addEventListener('DOMContentLoaded', (event) => { case 'add': barPage = barPage + '?add=1'; break; + case 'change': + barPage = barPage + '?change=1'; + break; default: break; } @@ -426,6 +470,11 @@ document.addEventListener('DOMContentLoaded', (event) => { command: 'bgAddClose', }); break; + case 'change': + sendPlatformMessage({ + command: 'bgChangeClose', + }); + break; default: break; } diff --git a/src/notification/bar.html b/src/notification/bar.html index 64dabbf1d32..c5ba4669e97 100644 --- a/src/notification/bar.html +++ b/src/notification/bar.html @@ -34,6 +34,16 @@ + + + + + + + +
+ +
diff --git a/src/notification/bar.js b/src/notification/bar.js index 4dfca0a66f4..24ca081f983 100644 --- a/src/notification/bar.js +++ b/src/notification/bar.js @@ -23,6 +23,8 @@ document.addEventListener('DOMContentLoaded', () => { i18n.notificationAddSave = chrome.i18n.getMessage('notificationAddSave'); i18n.notificationNeverSave = chrome.i18n.getMessage('notificationNeverSave'); i18n.notificationAddDesc = chrome.i18n.getMessage('notificationAddDesc'); + i18n.notificationChangeSave = chrome.i18n.getMessage('notificationChangeSave'); + i18n.notificationChangeDesc = chrome.i18n.getMessage('notificationChangeDesc'); // delay 50ms so that we get proper body dimensions setTimeout(load, 50); @@ -42,12 +44,15 @@ document.addEventListener('DOMContentLoaded', () => { if (bodyRect.width < 768) { document.querySelector('#template-add .add-save').textContent = i18n.yes; document.querySelector('#template-add .never-save').textContent = i18n.never; + document.querySelector('#template-change .change-save').textContent = i18n.yes; } else { document.querySelector('#template-add .add-save').textContent = i18n.notificationAddSave; document.querySelector('#template-add .never-save').textContent = i18n.notificationNeverSave; + document.querySelector('#template-change .change-save').textContent = i18n.notificationChangeSave; } document.querySelector('#template-add .add-text').textContent = i18n.notificationAddDesc; + document.querySelector('#template-change .change-text').textContent = i18n.notificationChangeDesc; if (getQueryVariable('add')) { setContent(document.getElementById('template-add')); @@ -68,6 +73,15 @@ document.addEventListener('DOMContentLoaded', () => { command: 'bgNeverSave' }); }); + } else if (getQueryVariable('change')) { + setContent(document.getElementById('template-change')); + var changeButton = document.querySelector('#template-change-clone .change-save'); + changeButton.addEventListener('click', (e) => { + e.preventDefault(); + sendPlatformMessage({ + command: 'bgChangeSave' + }); + }); } else if (getQueryVariable('info')) { setContent(document.getElementById('template-alert')); document.getElementById('template-alert-clone').textContent = getQueryVariable('info'); diff --git a/src/services/autofill.service.ts b/src/services/autofill.service.ts index 63aa65b7e8c..10a760c4ae2 100644 --- a/src/services/autofill.service.ts +++ b/src/services/autofill.service.ts @@ -119,24 +119,19 @@ export default class AutofillService implements AutofillServiceInterface { continue; } - for (let i = 0; i < passwordFields.length; i++) { - const pf = passwordFields[i]; - if (formKey !== pf.form) { - continue; - } - - let uf = this.findUsernameField(pageDetails, pf, false, false); + const formPasswordFields = passwordFields.filter((pf) => formKey === pf.form); + if (formPasswordFields.length > 0) { + let uf = this.findUsernameField(pageDetails, formPasswordFields[0], false, false); if (uf == null) { // not able to find any viewable username fields. maybe there are some "hidden" ones? - uf = this.findUsernameField(pageDetails, pf, true, false); + uf = this.findUsernameField(pageDetails, formPasswordFields[0], true, false); } - formData.push({ form: pageDetails.forms[formKey], - password: pf, + password: formPasswordFields[0], username: uf, + passwords: formPasswordFields, }); - break; } }