[EC-475] Auto-save password prompt enhancements (#4808)

* [EC-1062] Convert bar.js to TS and refactor (#4623)

* [EC-476 / EC-478] Add notificationBar edit flow (#4626)

* [EC-477] Enable auto-save for users without individual vault (#4760)

* [EC-1057] Add data loss warning to notificationBar edit flow (#4761)

* [AC-1173] Fix state bugs in auto-save edit flow (#4936)

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
Thomas Rittson 2023-03-09 08:12:43 +10:00 committed by GitHub
parent cafd2d2561
commit f592963191
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 537 additions and 343 deletions

View File

@ -10,8 +10,6 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import AddChangePasswordQueueMessage from "../../background/models/addChangePasswordQueueMessage";
import AddLoginQueueMessage from "../../background/models/addLoginQueueMessage";
@ -95,7 +93,7 @@ export default class NotificationBackground {
await BrowserApi.tabSendMessageData(sender.tab, "promptForLogin");
return;
}
await this.saveOrUpdateCredentials(sender.tab, msg.folder);
await this.saveOrUpdateCredentials(sender.tab, msg.edit, msg.folder);
break;
case "bgNeverSave":
await this.saveNever(sender.tab);
@ -168,6 +166,7 @@ export default class NotificationBackground {
typeData: {
isVaultLocked: this.notificationQueue[i].wasVaultLocked,
theme: await this.getCurrentTheme(),
removeIndividualVault: await this.removeIndividualVault(),
},
});
} else if (this.notificationQueue[i].type === NotificationQueueMessageType.ChangePassword) {
@ -225,10 +224,6 @@ export default class NotificationBackground {
return;
}
if (!(await this.allowPersonalOwnership())) {
return;
}
this.pushAddLoginToQueue(loginDomain, loginInfo, tab, true);
return;
}
@ -242,10 +237,6 @@ export default class NotificationBackground {
return;
}
if (!(await this.allowPersonalOwnership())) {
return;
}
this.pushAddLoginToQueue(loginDomain, loginInfo, tab);
} else if (
usernameMatches.length === 1 &&
@ -332,14 +323,10 @@ export default class NotificationBackground {
await this.checkNotificationQueue(tab);
}
private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, folderId?: string) {
private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, edit: boolean, folderId?: string) {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
const queueMessage = this.notificationQueue[i];
if (
queueMessage.tabId !== tab.id ||
(queueMessage.type !== NotificationQueueMessageType.AddLogin &&
queueMessage.type !== NotificationQueueMessageType.ChangePassword)
) {
if (queueMessage.tabId !== tab.id || !(queueMessage.type in NotificationQueueMessageType)) {
continue;
}
@ -352,63 +339,79 @@ export default class NotificationBackground {
BrowserApi.tabSendMessageData(tab, "closeNotificationBar");
if (queueMessage.type === NotificationQueueMessageType.ChangePassword) {
const changePasswordMessage = queueMessage as AddChangePasswordQueueMessage;
const cipher = await this.getDecryptedCipherById(changePasswordMessage.cipherId);
if (cipher == null) {
return;
}
await this.updateCipher(cipher, changePasswordMessage.newPassword);
const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId);
await this.updatePassword(cipherView, queueMessage.newPassword, edit, tab);
return;
}
if (queueMessage.type === NotificationQueueMessageType.AddLogin) {
if (!queueMessage.wasVaultLocked) {
await this.createNewCipher(queueMessage as AddLoginQueueMessage, folderId);
BrowserApi.tabSendMessageData(tab, "addedCipher");
return;
}
// If the vault was locked, check if a cipher needs updating instead of creating a new one
const addLoginMessage = queueMessage as AddLoginQueueMessage;
const ciphers = await this.cipherService.getAllDecryptedForUrl(addLoginMessage.uri);
const usernameMatches = ciphers.filter(
(c) =>
c.login.username != null && c.login.username.toLowerCase() === addLoginMessage.username
);
if (queueMessage.wasVaultLocked) {
const allCiphers = await this.cipherService.getAllDecryptedForUrl(queueMessage.uri);
const existingCipher = allCiphers.find(
(c) =>
c.login.username != null && c.login.username.toLowerCase() === queueMessage.username
);
if (usernameMatches.length >= 1) {
await this.updateCipher(usernameMatches[0], addLoginMessage.password);
if (existingCipher != null) {
await this.updatePassword(existingCipher, queueMessage.password, edit, tab);
return;
}
}
folderId = (await this.folderExists(folderId)) ? folderId : null;
const newCipher = AddLoginQueueMessage.toCipherView(queueMessage, folderId);
if (edit) {
await this.editItem(newCipher, tab);
return;
}
await this.createNewCipher(addLoginMessage, folderId);
const cipher = await this.cipherService.encrypt(newCipher);
await this.cipherService.createWithServer(cipher);
BrowserApi.tabSendMessageData(tab, "addedCipher");
}
}
}
private async createNewCipher(queueMessage: AddLoginQueueMessage, folderId: string) {
const loginModel = new LoginView();
const loginUri = new LoginUriView();
loginUri.uri = queueMessage.uri;
loginModel.uris = [loginUri];
loginModel.username = queueMessage.username;
loginModel.password = queueMessage.password;
const model = new CipherView();
model.name = Utils.getHostname(queueMessage.uri) || queueMessage.domain;
model.name = model.name.replace(/^www\./, "");
model.type = CipherType.Login;
model.login = loginModel;
private async updatePassword(
cipherView: CipherView,
newPassword: string,
edit: boolean,
tab: chrome.tabs.Tab
) {
cipherView.login.password = newPassword;
if (!Utils.isNullOrWhitespace(folderId)) {
const folders = await firstValueFrom(this.folderService.folderViews$);
if (folders.some((x) => x.id === folderId)) {
model.folderId = folderId;
}
if (edit) {
await this.editItem(cipherView, tab);
BrowserApi.tabSendMessage(tab, "editedCipher");
return;
}
const cipher = await this.cipherService.encrypt(model);
await this.cipherService.createWithServer(cipher);
const cipher = await this.cipherService.encrypt(cipherView);
await this.cipherService.updateWithServer(cipher);
// We've only updated the password, no need to broadcast editedCipher message
return;
}
private async editItem(cipherView: CipherView, senderTab: chrome.tabs.Tab) {
await this.stateService.setAddEditCipherInfo({
cipher: cipherView,
collectionIds: cipherView.collectionIds,
});
await BrowserApi.tabSendMessageData(senderTab, "openAddEditCipher", {
cipherId: cipherView.id,
});
}
private async folderExists(folderId: string) {
if (Utils.isNullOrWhitespace(folderId) || folderId === "null") {
return false;
}
const folders = await firstValueFrom(this.folderService.folderViews$);
return folders.some((x) => x.id === folderId);
}
private async getDecryptedCipherById(cipherId: string) {
@ -419,14 +422,6 @@ export default class NotificationBackground {
return null;
}
private async updateCipher(cipher: CipherView, newPassword: string) {
if (cipher != null && cipher.type === CipherType.Login) {
cipher.login.password = newPassword;
const newCipher = await this.cipherService.encrypt(cipher);
await this.cipherService.updateWithServer(newCipher);
}
}
private async saveNever(tab: chrome.tabs.Tab) {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
const queueMessage = this.notificationQueue[i];
@ -459,9 +454,9 @@ export default class NotificationBackground {
await BrowserApi.tabSendMessageData(tab, responseCommand, responseData);
}
private async allowPersonalOwnership(): Promise<boolean> {
return !(await firstValueFrom(
private async removeIndividualVault(): Promise<boolean> {
return await firstValueFrom(
this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
));
);
}
}

View File

@ -31,6 +31,7 @@ const forwardCommands = [
"addToLockedVaultPendingNotifications",
"unlockCompleted",
"addedCipher",
"openAddEditCipher",
];
chrome.runtime.onMessage.addListener((event) => {

View File

@ -502,6 +502,7 @@ document.addEventListener("DOMContentLoaded", (event) => {
type,
isVaultLocked: typeData.isVaultLocked,
theme: typeData.theme,
removeIndividualVault: typeData.removeIndividualVault,
};
const barQueryString = new URLSearchParams(barQueryParams).toString();
const barPage = "notification/bar.html?" + barQueryString;

View File

@ -28,21 +28,27 @@
</button>
</div>
</div>
<div id="templates" style="display: none">
<div class="inner-wrapper" id="template-add">
<div class="add-text"></div>
<div class="add-buttons">
<button type="button" class="never-save link"></button>
<select class="select-folder" isVaultLocked="false"></select>
<button type="button" class="add-save"></button>
</div>
</div>
<div class="inner-wrapper" id="template-change">
<div class="change-text"></div>
<div class="change-buttons">
<button type="button" class="change-save"></button>
</div>
</body>
<template id="template-add">
<div class="inner-wrapper">
<div id="add-text"></div>
<div>
<button type="button" id="never-save" class="link"></button>
<select id="select-folder"></select>
<button type="button" id="add-edit" class="secondary"></button>
<button type="button" id="add-save" class="primary"></button>
</div>
</div>
</body>
</template>
<template id="template-change">
<div class="inner-wrapper">
<div id="change-text"></div>
<div>
<button type="button" id="change-edit" class="secondary"></button>
<button type="button" id="change-save" class="primary"></button>
</div>
</div>
</template>
</html>

View File

@ -1,168 +0,0 @@
// eslint-disable-next-line
require("./bar.scss");
document.addEventListener("DOMContentLoaded", () => {
const theme = getQueryVariable("theme");
document.documentElement.classList.add("theme_" + theme);
let i18n = {};
let lang = window.navigator.language;
i18n.appName = chrome.i18n.getMessage("appName");
i18n.close = chrome.i18n.getMessage("close");
i18n.never = chrome.i18n.getMessage("never");
i18n.folder = chrome.i18n.getMessage("folder");
i18n.notificationAddSave = chrome.i18n.getMessage("notificationAddSave");
i18n.notificationAddDesc = chrome.i18n.getMessage("notificationAddDesc");
i18n.notificationChangeSave = chrome.i18n.getMessage("notificationChangeSave");
i18n.notificationChangeDesc = chrome.i18n.getMessage("notificationChangeDesc");
lang = chrome.i18n.getUILanguage(); // eslint-disable-line
// delay 50ms so that we get proper body dimensions
setTimeout(load, 50);
function load() {
const isVaultLocked = getQueryVariable("isVaultLocked") == "true";
document.getElementById("logo").src = isVaultLocked
? chrome.runtime.getURL("images/icon38_locked.png")
: chrome.runtime.getURL("images/icon38.png");
document.getElementById("logo-link").title = i18n.appName;
var neverButton = document.querySelector("#template-add .never-save");
neverButton.textContent = i18n.never;
var selectFolder = document.querySelector("#template-add .select-folder");
selectFolder.setAttribute("aria-label", i18n.folder);
selectFolder.setAttribute("isVaultLocked", isVaultLocked.toString());
var addButton = document.querySelector("#template-add .add-save");
addButton.textContent = i18n.notificationAddSave;
var changeButton = document.querySelector("#template-change .change-save");
changeButton.textContent = i18n.notificationChangeSave;
var closeButton = document.getElementById("close-button");
closeButton.title = i18n.close;
closeButton.setAttribute("aria-label", i18n.close);
document.querySelector("#template-add .add-text").textContent = i18n.notificationAddDesc;
document.querySelector("#template-change .change-text").textContent =
i18n.notificationChangeDesc;
if (getQueryVariable("type") === "add") {
handleTypeAdd(isVaultLocked);
} else if (getQueryVariable("type") === "change") {
handleTypeChange();
}
closeButton.addEventListener("click", (e) => {
e.preventDefault();
sendPlatformMessage({
command: "bgCloseNotificationBar",
});
});
window.addEventListener("resize", adjustHeight);
adjustHeight();
}
function getQueryVariable(variable) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
if (pair[0] === variable) {
return pair[1];
}
}
return null;
}
function handleTypeAdd(isVaultLocked) {
setContent(document.getElementById("template-add"));
var addButton = document.querySelector("#template-add-clone .add-save"), // eslint-disable-line
neverButton = document.querySelector("#template-add-clone .never-save"); // eslint-disable-line
addButton.addEventListener("click", (e) => {
e.preventDefault();
const folderId = document.querySelector("#template-add-clone .select-folder").value;
const bgAddSaveMessage = {
command: "bgAddSave",
folder: folderId,
};
sendPlatformMessage(bgAddSaveMessage);
});
neverButton.addEventListener("click", (e) => {
e.preventDefault();
sendPlatformMessage({
command: "bgNeverSave",
});
});
if (!isVaultLocked) {
const responseFoldersCommand = "notificationBarGetFoldersList";
chrome.runtime.onMessage.addListener((msg) => {
if (msg.command === responseFoldersCommand && msg.data) {
fillSelectorWithFolders(msg.data.folders);
}
});
sendPlatformMessage({
command: "bgGetDataForTab",
responseCommand: responseFoldersCommand,
});
}
}
function handleTypeChange() {
setContent(document.getElementById("template-change"));
var changeButton = document.querySelector("#template-change-clone .change-save"); // eslint-disable-line
changeButton.addEventListener("click", (e) => {
e.preventDefault();
const bgChangeSaveMessage = {
command: "bgChangeSave",
};
sendPlatformMessage(bgChangeSaveMessage);
});
}
function setContent(element) {
const content = document.getElementById("content");
while (content.firstChild) {
content.removeChild(content.firstChild);
}
var newElement = element.cloneNode(true);
newElement.id = newElement.id + "-clone";
content.appendChild(newElement);
}
function sendPlatformMessage(msg) {
chrome.runtime.sendMessage(msg);
}
function fillSelectorWithFolders(folders) {
const select = document.querySelector("#template-add-clone .select-folder");
select.appendChild(new Option(chrome.i18n.getMessage("selectFolder"), null, true));
folders.forEach((folder) => {
//Select "No Folder" (id=null) folder by default
select.appendChild(new Option(folder.name, folder.id || "", false));
});
}
function adjustHeight() {
sendPlatformMessage({
command: "bgAdjustNotificationBar",
data: {
height: document.querySelector("body").scrollHeight,
},
});
}
});

View File

@ -80,7 +80,7 @@ button {
cursor: pointer;
}
button:not(.neutral):not(.link) {
button.primary:not(.neutral) {
@include themify($themes) {
background-color: themed("primaryColor");
color: themed("textContrast");
@ -95,6 +95,21 @@ button:not(.neutral):not(.link) {
}
}
button.secondary:not(.neutral) {
@include themify($themes) {
background-color: themed("backgroundColor");
color: themed("mutedTextColor");
border-color: themed("mutedTextColor");
}
&:hover {
@include themify($themes) {
background-color: darken(themed("backgroundColor"), 1.5%);
color: darken(themed("mutedTextColor"), 6%);
}
}
}
button.link,
button.neutral {
@include themify($themes) {
@ -130,12 +145,8 @@ button {
font-family: $font-family-sans-serif;
}
.select-folder[isVaultLocked="true"] {
display: none;
}
@media screen and (max-width: 768px) {
.select-folder {
#select-folder {
display: none;
}
}

View File

@ -0,0 +1,218 @@
import type { Jsonify } from "type-fest";
import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
require("./bar.scss");
document.addEventListener("DOMContentLoaded", () => {
// delay 50ms so that we get proper body dimensions
setTimeout(load, 50);
});
function load() {
const theme = getQueryVariable("theme");
document.documentElement.classList.add("theme_" + theme);
const isVaultLocked = getQueryVariable("isVaultLocked") == "true";
(document.getElementById("logo") as HTMLImageElement).src = isVaultLocked
? chrome.runtime.getURL("images/icon38_locked.png")
: chrome.runtime.getURL("images/icon38.png");
const i18n = {
appName: chrome.i18n.getMessage("appName"),
close: chrome.i18n.getMessage("close"),
never: chrome.i18n.getMessage("never"),
folder: chrome.i18n.getMessage("folder"),
notificationAddSave: chrome.i18n.getMessage("notificationAddSave"),
notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"),
notificationEdit: chrome.i18n.getMessage("edit"),
notificationChangeSave: chrome.i18n.getMessage("notificationChangeSave"),
notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"),
};
document.getElementById("logo-link").title = i18n.appName;
// i18n for "Add" template
const addTemplate = document.getElementById("template-add") as HTMLTemplateElement;
const neverButton = addTemplate.content.getElementById("never-save");
neverButton.textContent = i18n.never;
const selectFolder = addTemplate.content.getElementById("select-folder");
selectFolder.hidden = isVaultLocked || removeIndividualVault();
selectFolder.setAttribute("aria-label", i18n.folder);
const addButton = addTemplate.content.getElementById("add-save");
addButton.textContent = i18n.notificationAddSave;
const addEditButton = addTemplate.content.getElementById("add-edit");
// If Remove Individual Vault policy applies, "Add" opens the edit tab, so we hide the Edit button
addEditButton.hidden = removeIndividualVault();
addEditButton.textContent = i18n.notificationEdit;
addTemplate.content.getElementById("add-text").textContent = i18n.notificationAddDesc;
// i18n for "Change" (update password) template
const changeTemplate = document.getElementById("template-change") as HTMLTemplateElement;
const changeButton = changeTemplate.content.getElementById("change-save");
changeButton.textContent = i18n.notificationChangeSave;
const changeEditButton = changeTemplate.content.getElementById("change-edit");
changeEditButton.textContent = i18n.notificationEdit;
changeTemplate.content.getElementById("change-text").textContent = i18n.notificationChangeDesc;
// i18n for body content
const closeButton = document.getElementById("close-button");
closeButton.title = i18n.close;
if (getQueryVariable("type") === "add") {
handleTypeAdd();
} else if (getQueryVariable("type") === "change") {
handleTypeChange();
}
closeButton.addEventListener("click", (e) => {
e.preventDefault();
sendPlatformMessage({
command: "bgCloseNotificationBar",
});
});
window.addEventListener("resize", adjustHeight);
adjustHeight();
}
function getQueryVariable(variable: string) {
const query = window.location.search.substring(1);
const vars = query.split("&");
for (let i = 0; i < vars.length; i++) {
const pair = vars[i].split("=");
if (pair[0] === variable) {
return pair[1];
}
}
return null;
}
function handleTypeAdd() {
setContent(document.getElementById("template-add") as HTMLTemplateElement);
const addButton = document.getElementById("add-save");
addButton.addEventListener("click", (e) => {
e.preventDefault();
// If Remove Individual Vault policy applies, "Add" opens the edit tab
sendPlatformMessage({
command: "bgAddSave",
folder: getSelectedFolder(),
edit: removeIndividualVault(),
});
});
if (removeIndividualVault()) {
// Everything past this point is only required if user has an individual vault
return;
}
const editButton = document.getElementById("add-edit");
editButton.addEventListener("click", (e) => {
e.preventDefault();
sendPlatformMessage({
command: "bgAddSave",
folder: getSelectedFolder(),
edit: true,
});
});
const neverButton = document.getElementById("never-save");
neverButton.addEventListener("click", (e) => {
e.preventDefault();
sendPlatformMessage({
command: "bgNeverSave",
});
});
loadFolderSelector();
}
function handleTypeChange() {
setContent(document.getElementById("template-change") as HTMLTemplateElement);
const changeButton = document.getElementById("change-save");
changeButton.addEventListener("click", (e) => {
e.preventDefault();
sendPlatformMessage({
command: "bgChangeSave",
edit: false,
});
});
const editButton = document.getElementById("change-edit");
editButton.addEventListener("click", (e) => {
e.preventDefault();
sendPlatformMessage({
command: "bgChangeSave",
edit: true,
});
});
}
function setContent(template: HTMLTemplateElement) {
const content = document.getElementById("content");
while (content.firstChild) {
content.removeChild(content.firstChild);
}
const newElement = template.content.cloneNode(true) as HTMLElement;
content.appendChild(newElement);
}
function sendPlatformMessage(msg: Record<string, unknown>) {
chrome.runtime.sendMessage(msg);
}
function loadFolderSelector() {
const responseFoldersCommand = "notificationBarGetFoldersList";
chrome.runtime.onMessage.addListener((msg) => {
if (msg.command !== responseFoldersCommand || msg.data == null) {
return;
}
const folders = msg.data.folders as Jsonify<FolderView[]>;
const select = document.getElementById("select-folder");
select.appendChild(new Option(chrome.i18n.getMessage("selectFolder"), null, true));
folders.forEach((folder) => {
// Select "No Folder" (id=null) folder by default
select.appendChild(new Option(folder.name, folder.id || "", false));
});
});
sendPlatformMessage({
command: "bgGetDataForTab",
responseCommand: responseFoldersCommand,
});
}
function getSelectedFolder(): string {
return (document.getElementById("select-folder") as HTMLSelectElement).value;
}
function removeIndividualVault(): boolean {
return getQueryVariable("removeIndividualVault") == "true";
}
function adjustHeight() {
sendPlatformMessage({
command: "bgAdjustNotificationBar",
data: {
height: document.querySelector("body").scrollHeight,
},
});
}

View File

@ -10,6 +10,7 @@ $brand-primary: #175ddc;
$background-color: #f0f0f0;
$solarizedDarkBase0: #839496;
$solarizedDarkBase03: #002b36;
$solarizedDarkBase02: #073642;
$solarizedDarkBase01: #586e75;
@ -20,6 +21,7 @@ $solarizedDarkGreen: #859900;
$themes: (
light: (
textColor: $text-color,
mutedTextColor: #6d757e,
backgroundColor: $background-color,
primaryColor: $brand-primary,
buttonPrimaryColor: $brand-primary,
@ -29,6 +31,7 @@ $themes: (
),
dark: (
textColor: #ffffff,
mutedTextColor: #bac0ce,
backgroundColor: #2f343d,
buttonPrimaryColor: #6f9df1,
primaryColor: #6f9df1,
@ -38,6 +41,7 @@ $themes: (
),
nord: (
textColor: $nord5,
mutedTextColor: $nord4,
backgroundColor: $nord1,
buttonPrimaryColor: $nord8,
primaryColor: $nord9,
@ -47,6 +51,8 @@ $themes: (
),
solarizedDark: (
textColor: $solarizedDarkBase2,
// Muted uses main text color to avoid contrast issues
mutedTextColor: $solarizedDarkBase2,
backgroundColor: $solarizedDarkBase03,
buttonPrimaryColor: $solarizedDarkCyan,
primaryColor: $solarizedDarkGreen,

View File

@ -96,7 +96,6 @@ import { SafariApp } from "../browser/safariApp";
import { flagEnabled } from "../flags";
import { UpdateBadge } from "../listeners/update-badge";
import { Account } from "../models/account";
import { PopupUtilsService } from "../popup/services/popup-utils.service";
import { BrowserStateService as StateServiceAbstraction } from "../services/abstractions/browser-state.service";
import { BrowserEnvironmentService } from "../services/browser-environment.service";
import { BrowserI18nService } from "../services/browser-i18n.service";
@ -157,7 +156,6 @@ export default class MainBackground {
eventCollectionService: EventCollectionServiceAbstraction;
eventUploadService: EventUploadServiceAbstraction;
policyService: InternalPolicyServiceAbstraction;
popupUtilsService: PopupUtilsService;
sendService: SendServiceAbstraction;
fileUploadService: FileUploadServiceAbstraction;
organizationService: InternalOrganizationServiceAbstraction;
@ -358,7 +356,7 @@ export default class MainBackground {
// AuthService should send the messages to the background not popup.
send = (subscriber: string, arg: any = {}) => {
const message = Object.assign({}, { command: subscriber }, arg);
that.runtimeBackground.processMessage(message, that, null);
that.runtimeBackground.processMessage(message, that as any, null);
};
})();
this.authService = new AuthService(
@ -463,7 +461,6 @@ export default class MainBackground {
this.authService,
this.messagingService
);
this.popupUtilsService = new PopupUtilsService(isPrivateMode);
this.userVerificationApiService = new UserVerificationApiService(this.apiService);

View File

@ -1,6 +1,8 @@
import NotificationQueueMessage from "./notificationQueueMessage";
import { NotificationQueueMessageType } from "./notificationQueueMessageType";
export default class AddChangePasswordQueueMessage extends NotificationQueueMessage {
type: NotificationQueueMessageType.ChangePassword;
cipherId: string;
newPassword: string;
}

View File

@ -1,7 +1,33 @@
import { Utils } from "@bitwarden/common/misc/utils";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import NotificationQueueMessage from "./notificationQueueMessage";
import { NotificationQueueMessageType } from "./notificationQueueMessageType";
export default class AddLoginQueueMessage extends NotificationQueueMessage {
type: NotificationQueueMessageType.AddLogin;
username: string;
password: string;
uri: string;
static toCipherView(message: AddLoginQueueMessage, folderId?: string): CipherView {
const uriView = new LoginUriView();
uriView.uri = message.uri;
const loginView = new LoginView();
loginView.uris = [uriView];
loginView.username = message.username;
loginView.password = message.password;
const cipherView = new CipherView();
cipherView.name = (Utils.getHostname(message.uri) || message.domain).replace(/^www\./, "");
cipherView.folderId = folderId;
cipherView.type = CipherType.Login;
cipherView.login = loginView;
return cipherView;
}
}

View File

@ -56,19 +56,15 @@ export default class RuntimeBackground {
}
}
async processMessage(msg: any, sender: any, sendResponse: any) {
async processMessage(msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) {
switch (msg.command) {
case "loggedIn":
case "unlocked": {
let item: LockedVaultPendingNotificationsItem;
if (this.lockedVaultPendingNotifications?.length > 0) {
await BrowserApi.closeLoginTab();
item = this.lockedVaultPendingNotifications.pop();
if (item.commandToRetry.sender?.tab?.id) {
await BrowserApi.focusSpecifiedTab(item.commandToRetry.sender.tab.id);
}
BrowserApi.closeBitwardenExtensionTab();
}
await this.main.refreshBadge();
@ -104,7 +100,21 @@ export default class RuntimeBackground {
await this.main.openPopup();
break;
case "promptForLogin":
await BrowserApi.createNewTab("popup/index.html?uilocation=popout", true, true);
BrowserApi.openBitwardenExtensionTab("popup/index.html", true, sender.tab);
break;
case "openAddEditCipher": {
const addEditCipherUrl =
msg.data?.cipherId == null
? "popup/index.html#/edit-cipher"
: "popup/index.html#/edit-cipher?cipherId=" + msg.data.cipherId;
BrowserApi.openBitwardenExtensionTab(addEditCipherUrl, true, sender.tab);
break;
}
case "closeTab":
setTimeout(() => {
BrowserApi.closeBitwardenExtensionTab();
}, msg.delay ?? 0);
break;
case "showDialogResolve":
this.platformUtilsService.resolveDialogPromise(msg.dialogId, msg.confirmed);
@ -183,11 +193,7 @@ export default class RuntimeBackground {
const params =
`webAuthnResponse=${encodeURIComponent(msg.data)};` +
`remember=${encodeURIComponent(msg.remember)}`;
BrowserApi.createNewTab(
`popup/index.html?uilocation=popout#/2fa;${params}`,
undefined,
false
);
BrowserApi.openBitwardenExtensionTab(`popup/index.html#/2fa;${params}`, false);
break;
}
case "reloadPopup":

View File

@ -127,8 +127,44 @@ export class BrowserApi {
return Promise.resolve(chrome.extension.getViews({ type: "popup" }).length > 0);
}
static createNewTab(url: string, extensionPage = false, active = true) {
chrome.tabs.create({ url: url, active: active });
static createNewTab(url: string, active = true, openerTab?: chrome.tabs.Tab) {
chrome.tabs.create({ url: url, active: active, openerTabId: openerTab?.id });
}
static openBitwardenExtensionTab(
relativeUrl: string,
active = true,
openerTab?: chrome.tabs.Tab
) {
if (relativeUrl.includes("uilocation=tab")) {
this.createNewTab(relativeUrl, active, openerTab);
return;
}
const fullUrl = chrome.extension.getURL(relativeUrl);
const parsedUrl = new URL(fullUrl);
parsedUrl.searchParams.set("uilocation", "tab");
this.createNewTab(parsedUrl.toString(), active, openerTab);
}
static async closeBitwardenExtensionTab() {
const tabs = await BrowserApi.tabsQuery({
active: true,
title: "Bitwarden",
windowType: "normal",
currentWindow: true,
});
if (tabs.length === 0) {
return;
}
const tabToClose = tabs[tabs.length - 1];
chrome.tabs.remove(tabToClose.id);
if (tabToClose.openerTabId) {
this.focusTab(tabToClose.openerTabId);
}
}
static messageListener(
@ -147,23 +183,7 @@ export class BrowserApi {
return chrome.runtime.sendMessage(message);
}
static async closeLoginTab() {
const tabs = await BrowserApi.tabsQuery({
active: true,
title: "Bitwarden",
windowType: "normal",
currentWindow: true,
});
if (tabs.length === 0) {
return;
}
const tabToClose = tabs[tabs.length - 1].id;
chrome.tabs.remove(tabToClose);
}
static async focusSpecifiedTab(tabId: number) {
static async focusTab(tabId: number) {
chrome.tabs.update(tabId, { active: true, highlighted: true });
}

View File

@ -10,13 +10,14 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { UsernameGenerationService } from "@bitwarden/common/abstractions/usernameGeneration.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info";
@Component({
selector: "app-generator",
templateUrl: "generator.component.html",
})
export class GeneratorComponent extends BaseGeneratorComponent {
private addEditCipherInfo: any;
private addEditCipherInfo: AddEditCipherInfo;
private cipherState: CipherView;
constructor(

View File

@ -408,28 +408,10 @@ app-root {
}
}
// Adds padding on each side of the content if opened in a tab
@media only screen and (min-width: 601px) {
app-login header {
padding: 0 calc((100% - 500px) / 2);
}
app-login main {
padding: 0 calc((100% - 500px) / 2);
}
app-two-factor header {
padding: 0 calc((100% - 500px) / 2);
}
app-two-factor main {
padding: 0 calc((100% - 500px) / 2);
}
app-lock header {
padding: 0 calc((100% - 500px) / 2);
}
app-lock main {
header,
main {
padding: 0 calc((100% - 500px) / 2);
}
}

View File

@ -1,9 +1,12 @@
import { Injectable } from "@angular/core";
import { fromEvent, Subscription } from "rxjs";
import { BrowserApi } from "../../browser/browserApi";
@Injectable()
export class PopupUtilsService {
private unloadSubscription: Subscription;
constructor(private privateMode: boolean = false) {}
inSidebar(win: Window): boolean {
@ -80,4 +83,36 @@ export class PopupUtilsService {
});
}
}
/**
* Enables a pop-up warning before the user exits the window/tab, or navigates away.
* This warns the user that they may lose unsaved data if they leave the page.
* (Note: navigating within the Angular app will not trigger it because it's an SPA.)
* Make sure you call `disableTabCloseWarning` when it is no longer relevant.
*/
enableCloseTabWarning() {
this.disableCloseTabWarning();
this.unloadSubscription = fromEvent(window, "beforeunload").subscribe(
(e: BeforeUnloadEvent) => {
// Recommended method but not widely supported
e.preventDefault();
// Modern browsers do not display this message, it just needs to be a non-nullish value
// Exact wording is determined by the browser
const confirmationMessage = "";
// Older methods with better support
e.returnValue = confirmationMessage;
return confirmationMessage;
}
);
}
/**
* Disables the warning enabled by enableCloseTabWarning.
*/
disableCloseTabWarning() {
this.unloadSubscription?.unsubscribe();
}
}

View File

@ -130,15 +130,11 @@ export class AddEditComponent extends BaseAddEditComponent {
: tabs.filter((tab) => tab.url != null && tab.url !== "").map((tab) => tab.url);
}
window.setTimeout(() => {
if (!this.editMode) {
if (this.cipher.name != null && this.cipher.name !== "") {
document.getElementById("loginUsername").focus();
} else {
document.getElementById("name").focus();
}
}
}, 200);
this.setFocus();
if (this.popupUtilsService.inTab(window)) {
this.popupUtilsService.enableCloseTabWarning();
}
}
async load() {
@ -149,16 +145,23 @@ export class AddEditComponent extends BaseAddEditComponent {
}
async submit(): Promise<boolean> {
if (await super.submit()) {
if (this.cloneMode) {
this.router.navigate(["/tabs/vault"]);
} else {
this.location.back();
}
const success = await super.submit();
if (!success) {
return false;
}
if (this.popupUtilsService.inTab(window)) {
this.popupUtilsService.disableCloseTabWarning();
this.messagingService.send("closeTab", { delay: 1000 });
return true;
}
return false;
if (this.cloneMode) {
this.router.navigate(["/tabs/vault"]);
} else {
this.location.back();
}
return true;
}
attachments() {
@ -184,6 +187,12 @@ export class AddEditComponent extends BaseAddEditComponent {
cancel() {
super.cancel();
if (this.popupUtilsService.inTab(window)) {
this.messagingService.send("closeTab");
return;
}
this.location.back();
}
@ -235,4 +244,18 @@ export class AddEditComponent extends BaseAddEditComponent {
: this.collections.filter((c) => (c as any).checked).map((c) => c.id),
});
}
private setFocus() {
window.setTimeout(() => {
if (this.editMode) {
return;
}
if (this.cipher.name != null && this.cipher.name !== "") {
document.getElementById("loginUsername").focus();
} else {
document.getElementById("name").focus();
}
}, 200);
}
}

View File

@ -146,7 +146,7 @@ const mainConfig = {
"content/notificationBar": "./src/autofill/content/notification-bar.ts",
"content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts",
"content/message_handler": "./src/autofill/content/message_handler.ts",
"notification/bar": "./src/autofill/notification/bar.js",
"notification/bar": "./src/autofill/notification/bar.ts",
"encrypt-worker": "../../libs/common/src/services/cryptography/encrypt.worker.ts",
},
optimization: {

View File

@ -202,7 +202,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
});
if (!this.allowPersonal) {
this.organizationId = this.ownershipOptions[0].value;
this.organizationId = this.defaultOwnerId;
}
}
@ -220,12 +220,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.title = this.i18nService.t("addItem");
}
const addEditCipherInfo: any = await this.stateService.getAddEditCipherInfo();
if (addEditCipherInfo != null) {
this.cipher = addEditCipherInfo.cipher;
this.collectionIds = addEditCipherInfo.collectionIds;
}
await this.stateService.setAddEditCipherInfo(null);
const loadedAddEditCipherInfo = await this.loadAddEditCipherInfo();
if (this.cipher == null) {
if (this.editMode) {
@ -255,7 +250,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
}
if (this.cipher != null && (!this.editMode || addEditCipherInfo != null || this.cloneMode)) {
if (this.cipher != null && (!this.editMode || loadedAddEditCipherInfo || this.cloneMode)) {
await this.organizationChanged();
if (
this.collectionIds != null &&
@ -618,4 +613,27 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected restoreCipher() {
return this.cipherService.restoreWithServer(this.cipher.id);
}
get defaultOwnerId(): string | null {
return this.ownershipOptions[0].value;
}
async loadAddEditCipherInfo(): Promise<boolean> {
const addEditCipherInfo: any = await this.stateService.getAddEditCipherInfo();
const loadedSavedInfo = addEditCipherInfo != null;
if (loadedSavedInfo) {
this.cipher = addEditCipherInfo.cipher;
this.collectionIds = addEditCipherInfo.collectionIds;
if (!this.editMode && !this.allowPersonal && this.cipher.organizationId == null) {
// This is a new cipher and personal ownership isn't allowed, so we need to set the default owner
this.cipher.organizationId = this.defaultOwnerId;
}
}
await this.stateService.setAddEditCipherInfo(null);
return loadedSavedInfo;
}
}

View File

@ -26,6 +26,7 @@ import { CipherData } from "../vault/models/data/cipher.data";
import { FolderData } from "../vault/models/data/folder.data";
import { LocalData } from "../vault/models/data/local.data";
import { CipherView } from "../vault/models/view/cipher.view";
import { AddEditCipherInfo } from "../vault/types/add-edit-cipher-info";
export abstract class StateService<T extends Account = Account> {
accounts$: Observable<{ [userId: string]: T }>;
@ -39,8 +40,8 @@ export abstract class StateService<T extends Account = Account> {
getAccessToken: (options?: StorageOptions) => Promise<string>;
setAccessToken: (value: string, options?: StorageOptions) => Promise<void>;
getAddEditCipherInfo: (options?: StorageOptions) => Promise<any>;
setAddEditCipherInfo: (value: any, options?: StorageOptions) => Promise<void>;
getAddEditCipherInfo: (options?: StorageOptions) => Promise<AddEditCipherInfo>;
setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>;
getAlwaysShowDock: (options?: StorageOptions) => Promise<boolean>;
setAlwaysShowDock: (value: boolean, options?: StorageOptions) => Promise<void>;
getApiKeyClientId: (options?: StorageOptions) => Promise<string>;

View File

@ -45,6 +45,7 @@ import { CipherData } from "../vault/models/data/cipher.data";
import { FolderData } from "../vault/models/data/folder.data";
import { LocalData } from "../vault/models/data/local.data";
import { CipherView } from "../vault/models/view/cipher.view";
import { AddEditCipherInfo } from "../vault/types/add-edit-cipher-info";
const keys = {
state: "state",
@ -222,13 +223,13 @@ export class StateService<
await this.saveAccount(account, options);
}
async getAddEditCipherInfo(options?: StorageOptions): Promise<any> {
async getAddEditCipherInfo(options?: StorageOptions): Promise<AddEditCipherInfo> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.data?.addEditCipherInfo;
}
async setAddEditCipherInfo(value: any, options?: StorageOptions): Promise<void> {
async setAddEditCipherInfo(value: AddEditCipherInfo, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);

View File

@ -0,0 +1,12 @@
import { CipherView } from "../models/view/cipher.view";
/**
* Used to temporarily save the state of the AddEditComponent, e.g. when the user navigates away to the Generator page.
* @property cipher The unsaved item being added or edited
* @property collectionIds The collections that are selected for the item (currently these are not mapped back to
* cipher.collectionIds until the item is saved)
*/
export type AddEditCipherInfo = {
cipher: CipherView;
collectionIds?: string[];
};