1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-25 12:15:18 +01:00

[PM-2753] Prompt to Save New Login Credentials Silently Drops Data on Network Error (#7730)

* [PM-673] Safari Notification Bar Does Not Show Folders

* [PM-673] Refactoring Context Menu Implementations to Ensure Pages with No Logins Can Dismiss Notification Bar

* [PM-673] Refactoring typing information for the LockedVaultPendingNotificationsItem typing

* [PM-673] Refactoring typing information for notification background

* [PM-673] Finishing out typing for potential extension messages to the notification bar;

* [PM-673] Working through implementation details for the notification background

* [PM-673] Fixing issues present with messaging re-implementation

* [PM-673] Fixing issue with folders not populating within Safari notification bar

* [PM-673] Fixing jest test issues present within implementation

* [PM-673] Fixing issue present with webVaultUrl vulnerability

* [PM-673] Fixing XSS Vulnerability within Notification Bar;

* [PM-5670] Putting together a partial implementation for having messages appear on network error within the notification bar

* [PM-673] Incorporating status update for when user has successfully saved credentials

* [PM-673] Incorporating status update for when user has successfully saved credentials

* [PM-5949] Refactor typing information for notification bar

* [PM-5949] Fix jest tests for overlay background

* [PM-5949] Removing unnused typing data

* [PM-5949] Fixing lint error

* [PM-5949] Adding jest tests for convertAddLoginQueueMessageToCipherView method

* [PM-5949] Fixing jest test for overlay

* [PM-5950] Fix Context Menu Update Race Condition and Refactor Implementation

* [PM-5950] Adding jest test for cipherContextMenu.update method

* [PM-5950] Adding documentation for method within MainContextMenuHandler

* [PM-5950] Adding jest tests for the mainContextMenuHandler

* [PM-673] Stripping unnecessary work for network drop issue

* [PM-673] Stripping unnecessary work for network drop issue

* [PM-2753] Prompt to Save New Login Credentials Silently Drops Data on Network Error

* [PM-673] Stripping out work done for another ticket

* [PM-5950] Removing unnecessary return value from MainContextMenuHandler.create method

* [PM-673] Implementing unit test coverage for newly introduced logic

* [PM-673] Implementing unit test coverage for newly introduced logic

* [PM-673] Implementing unit test coverage for newly introduced logic

* [PM-673] Implementing unit test coverage for newly introduced logic

* [PM-2753] Implementing jest tests to validate logic changes

* [PM-2753] Implementing jest tests to validate logic changes

* [PM-2753] Implementing jest tests to validate logic changes

* [PM-2753] Implementing jest tests to validate logic changes

* [PM-2753] Incorporating addition of green and red borders when success or error events occur

* [PM-5950] Fixing unawaited context menu promise

* [PM-673] Merging changes in from main and fixing merge conflicts

* [PM-2753] Merging work in from main and resolving merge conflicts

* [PM-673] Fixing issue where updates to the added login were not triggering correctly

* [PM-673] Merging changes in from main and fixing merge conflicts
This commit is contained in:
Cesar Gonzalez 2024-02-23 10:50:11 -06:00 committed by GitHub
parent 43d1174a06
commit 3367339f9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 618 additions and 43 deletions

View File

@ -2542,8 +2542,8 @@
"description": "LastPass specific notification button text for cancelling a fileless import." "description": "LastPass specific notification button text for cancelling a fileless import."
}, },
"startFilelessImport": { "startFilelessImport": {
"message": "Import to Bitwarden", "message": "Import to Bitwarden",
"description": "Notification button text for starting a fileless import." "description": "Notification button text for starting a fileless import."
}, },
"importing": { "importing": {
"message": "Importing...", "message": "Importing...",
@ -2993,5 +2993,17 @@
"makeDefault": { "makeDefault": {
"message": "Make default", "message": "Make default",
"description": "Button text for the setting that allows overriding the default browser autofill settings" "description": "Button text for the setting that allows overriding the default browser autofill settings"
},
"saveCipherAttemptSuccess": {
"message": "Credentials saved successfully!",
"description": "Notification message for when saving credentials has succeeded."
},
"updateCipherAttemptSuccess": {
"message": "Credentials updated successfully!",
"description": "Notification message for when updating credentials has succeeded."
},
"saveCipherAttemptFailed": {
"message": "Error saving credentials. Check console for details.",
"description": "Notification message for when saving credentials has failed."
} }
} }

View File

@ -88,6 +88,8 @@ type NotificationBackgroundExtensionMessage = {
notificationType?: string; notificationType?: string;
}; };
type SaveOrUpdateCipherResult = undefined | { error: string };
type BackgroundMessageParam = { message: NotificationBackgroundExtensionMessage }; type BackgroundMessageParam = { message: NotificationBackgroundExtensionMessage };
type BackgroundSenderParam = { sender: chrome.runtime.MessageSender }; type BackgroundSenderParam = { sender: chrome.runtime.MessageSender };
type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam;
@ -120,6 +122,7 @@ export {
ChangePasswordMessageData, ChangePasswordMessageData,
UnlockVaultMessageData, UnlockVaultMessageData,
AddLoginMessageData, AddLoginMessageData,
SaveOrUpdateCipherResult,
NotificationBackgroundExtensionMessage, NotificationBackgroundExtensionMessage,
NotificationBackgroundExtensionMessageHandlers, NotificationBackgroundExtensionMessageHandlers,
}; };

View File

@ -20,6 +20,7 @@ import { createAutofillPageDetailsMock, createChromeTabMock } from "../spec/auto
import { flushPromises, sendExtensionRuntimeMessage } from "../spec/testing-utils"; import { flushPromises, sendExtensionRuntimeMessage } from "../spec/testing-utils";
import { import {
AddChangePasswordQueueMessage,
AddLoginQueueMessage, AddLoginQueueMessage,
AddUnlockVaultQueueMessage, AddUnlockVaultQueueMessage,
LockedVaultPendingNotificationsData, LockedVaultPendingNotificationsData,
@ -640,6 +641,424 @@ describe("NotificationBackground", () => {
}); });
}); });
describe("bgSaveCipher message handler", () => {
let getAuthStatusSpy: jest.SpyInstance;
let tabSendMessageDataSpy: jest.SpyInstance;
let openUnlockPopoutSpy: jest.SpyInstance;
beforeEach(() => {
getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus");
tabSendMessageDataSpy = jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
openUnlockPopoutSpy = jest
.spyOn(notificationBackground as any, "openUnlockPopout")
.mockImplementation();
});
it("skips saving the cipher and opens an unlock popout if the extension is not unlocked", async () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(tabSendMessageDataSpy).toHaveBeenCalledWith(
sender.tab,
"addToLockedVaultPendingNotifications",
{
commandToRetry: { message, sender },
target: "notification.background",
},
);
expect(openUnlockPopoutSpy).toHaveBeenCalledWith(sender.tab);
});
describe("saveOrUpdateCredentials", () => {
let getDecryptedCipherByIdSpy: jest.SpyInstance;
let getAllDecryptedForUrlSpy: jest.SpyInstance;
let updatePasswordSpy: jest.SpyInstance;
let convertAddLoginQueueMessageToCipherViewSpy: jest.SpyInstance;
let tabSendMessageSpy: jest.SpyInstance;
let editItemSpy: jest.SpyInstance;
let setAddEditCipherInfoSpy: jest.SpyInstance;
let openAddEditVaultItemPopoutSpy: jest.SpyInstance;
let createWithServerSpy: jest.SpyInstance;
let updateWithServerSpy: jest.SpyInstance;
let folderExistsSpy: jest.SpyInstance;
let cipherEncryptSpy: jest.SpyInstance;
beforeEach(() => {
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getDecryptedCipherByIdSpy = jest.spyOn(
notificationBackground as any,
"getDecryptedCipherById",
);
getAllDecryptedForUrlSpy = jest.spyOn(cipherService, "getAllDecryptedForUrl");
updatePasswordSpy = jest.spyOn(notificationBackground as any, "updatePassword");
convertAddLoginQueueMessageToCipherViewSpy = jest.spyOn(
notificationBackground as any,
"convertAddLoginQueueMessageToCipherView",
);
tabSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage").mockImplementation();
editItemSpy = jest.spyOn(notificationBackground as any, "editItem");
setAddEditCipherInfoSpy = jest.spyOn(stateService, "setAddEditCipherInfo");
openAddEditVaultItemPopoutSpy = jest.spyOn(
notificationBackground as any,
"openAddEditVaultItemPopout",
);
createWithServerSpy = jest.spyOn(cipherService, "createWithServer");
updateWithServerSpy = jest.spyOn(cipherService, "updateWithServer");
folderExistsSpy = jest.spyOn(notificationBackground as any, "folderExists");
cipherEncryptSpy = jest.spyOn(cipherService, "encrypt");
});
it("skips saving the cipher if the notification queue does not have a tab that is related to the sender", async () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 2 } });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
notificationBackground["notificationQueue"] = [
mock<AddLoginQueueMessage>({
tab: createChromeTabMock({ id: 1 }),
}),
];
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(updatePasswordSpy).not.toHaveBeenCalled();
expect(editItemSpy).not.toHaveBeenCalled();
expect(createWithServerSpy).not.toHaveBeenCalled();
});
it("skips saving the cipher if the notification queue does not contain an AddLogin or ChangePassword type", async () => {
const tab = createChromeTabMock({ id: 1 });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
notificationBackground["notificationQueue"] = [
mock<AddUnlockVaultQueueMessage>({
tab,
type: NotificationQueueMessageType.UnlockVault,
}),
];
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(updatePasswordSpy).not.toHaveBeenCalled();
expect(editItemSpy).not.toHaveBeenCalled();
expect(createWithServerSpy).not.toHaveBeenCalled();
});
it("skips saving the cipher if the notification queue message has a different domain than the passed tab", () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
notificationBackground["notificationQueue"] = [
mock<AddLoginQueueMessage>({
type: NotificationQueueMessageType.AddLogin,
tab,
domain: "another.com",
}),
];
sendExtensionRuntimeMessage(message, sender);
expect(updatePasswordSpy).not.toHaveBeenCalled();
expect(editItemSpy).not.toHaveBeenCalled();
expect(createWithServerSpy).not.toHaveBeenCalled();
});
it("updates the password if the notification message type is for ChangePassword", async () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
const queueMessage = mock<AddChangePasswordQueueMessage>({
type: NotificationQueueMessageType.ChangePassword,
tab,
domain: "example.com",
newPassword: "newPassword",
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>();
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(editItemSpy).not.toHaveBeenCalled();
expect(createWithServerSpy).not.toHaveBeenCalled();
expect(updatePasswordSpy).toHaveBeenCalledWith(
cipherView,
queueMessage.newPassword,
message.edit,
sender.tab,
);
expect(updateWithServerSpy).toHaveBeenCalled();
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
command: "saveCipherAttemptCompleted",
});
});
it("updates the cipher password if the queue message was locked and an existing cipher has the same username as the message", async () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
const queueMessage = mock<AddLoginQueueMessage>({
type: NotificationQueueMessageType.AddLogin,
tab,
domain: "example.com",
username: "test",
password: "updated-password",
wasVaultLocked: true,
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({
login: { username: "test", password: "old-password" },
});
getAllDecryptedForUrlSpy.mockResolvedValueOnce([cipherView]);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(updatePasswordSpy).toHaveBeenCalledWith(
cipherView,
queueMessage.password,
message.edit,
sender.tab,
);
expect(editItemSpy).not.toHaveBeenCalled();
expect(createWithServerSpy).not.toHaveBeenCalled();
});
it("opens an editItem window and closes the notification bar if the edit value is within the passed message when attempting to update an existing cipher", async () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: true,
folder: "folder-id",
};
const queueMessage = mock<AddChangePasswordQueueMessage>({
type: NotificationQueueMessageType.ChangePassword,
tab,
domain: "example.com",
newPassword: "newPassword",
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>();
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
setAddEditCipherInfoSpy.mockResolvedValue(undefined);
openAddEditVaultItemPopoutSpy.mockResolvedValue(undefined);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(updatePasswordSpy).toHaveBeenCalledWith(
cipherView,
queueMessage.newPassword,
message.edit,
sender.tab,
);
expect(editItemSpy).toHaveBeenCalled();
expect(updateWithServerSpy).not.toHaveBeenCalled();
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
command: "closeNotificationBar",
});
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
command: "editedCipher",
});
expect(setAddEditCipherInfoSpy).toHaveBeenCalledWith({
cipher: cipherView,
collectionIds: cipherView.collectionIds,
});
expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalledWith(sender.tab, {
cipherId: cipherView.id,
});
});
it("opens an editItem window and closes the notification bar if the edit value is within the passed message when attempting to save the cipher", async () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: true,
folder: "folder-id",
};
const queueMessage = mock<AddLoginQueueMessage>({
type: NotificationQueueMessageType.AddLogin,
tab,
domain: "example.com",
username: "test",
password: "password",
wasVaultLocked: false,
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({
login: { username: "test", password: "password" },
});
folderExistsSpy.mockResolvedValueOnce(true);
convertAddLoginQueueMessageToCipherViewSpy.mockReturnValueOnce(cipherView);
editItemSpy.mockResolvedValueOnce(undefined);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(updatePasswordSpy).not.toHaveBeenCalled();
expect(convertAddLoginQueueMessageToCipherViewSpy).toHaveBeenCalledWith(
queueMessage,
message.folder,
);
expect(editItemSpy).toHaveBeenCalledWith(cipherView, sender.tab);
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
command: "closeNotificationBar",
});
expect(createWithServerSpy).not.toHaveBeenCalled();
});
it("creates the cipher within the server and sends an `saveCipherAttemptCompleted` and `addedCipher` message to the sender tab", async () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
const queueMessage = mock<AddLoginQueueMessage>({
type: NotificationQueueMessageType.AddLogin,
tab,
domain: "example.com",
username: "test",
password: "password",
wasVaultLocked: false,
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({
login: { username: "test", password: "password" },
});
folderExistsSpy.mockResolvedValueOnce(false);
convertAddLoginQueueMessageToCipherViewSpy.mockReturnValueOnce(cipherView);
editItemSpy.mockResolvedValueOnce(undefined);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(convertAddLoginQueueMessageToCipherViewSpy).toHaveBeenCalledWith(
queueMessage,
null,
);
expect(cipherEncryptSpy).toHaveBeenCalledWith(cipherView);
expect(createWithServerSpy).toHaveBeenCalled();
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
command: "saveCipherAttemptCompleted",
});
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { command: "addedCipher" });
});
it("sends an error message within the `saveCipherAttemptCompleted` message if the cipher cannot be saved to the server", async () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
const queueMessage = mock<AddLoginQueueMessage>({
type: NotificationQueueMessageType.AddLogin,
tab,
domain: "example.com",
username: "test",
password: "password",
wasVaultLocked: false,
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({
login: { username: "test", password: "password" },
});
folderExistsSpy.mockResolvedValueOnce(true);
convertAddLoginQueueMessageToCipherViewSpy.mockReturnValueOnce(cipherView);
editItemSpy.mockResolvedValueOnce(undefined);
const errorMessage = "fetch error";
createWithServerSpy.mockImplementation(() => {
throw new Error(errorMessage);
});
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(cipherEncryptSpy).toHaveBeenCalledWith(cipherView);
expect(createWithServerSpy).toThrow(errorMessage);
expect(tabSendMessageSpy).not.toHaveBeenCalledWith(sender.tab, {
command: "addedCipher",
});
expect(tabSendMessageDataSpy).toHaveBeenCalledWith(
sender.tab,
"saveCipherAttemptCompleted",
{
error: errorMessage,
},
);
});
it("sends an error message within the `saveCipherAttemptCompleted` message if the cipher cannot be updated within the server", async () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
const queueMessage = mock<AddChangePasswordQueueMessage>({
type: NotificationQueueMessageType.ChangePassword,
tab,
domain: "example.com",
newPassword: "newPassword",
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>();
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
const errorMessage = "fetch error";
updateWithServerSpy.mockImplementation(() => {
throw new Error(errorMessage);
});
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(updateWithServerSpy).toThrow(errorMessage);
expect(tabSendMessageDataSpy).toHaveBeenCalledWith(
sender.tab,
"saveCipherAttemptCompleted",
{
error: errorMessage,
},
);
});
});
});
describe("bgNeverSave message handler", () => { describe("bgNeverSave message handler", () => {
let tabSendMessageDataSpy: jest.SpyInstance; let tabSendMessageDataSpy: jest.SpyInstance;

View File

@ -39,6 +39,7 @@ import { OverlayBackgroundExtensionMessage } from "./abstractions/overlay.backgr
export default class NotificationBackground { export default class NotificationBackground {
private openUnlockPopout = openUnlockPopout; private openUnlockPopout = openUnlockPopout;
private openAddEditVaultItemPopout = openAddEditVaultItemPopout;
private notificationQueue: NotificationQueueMessageItem[] = []; private notificationQueue: NotificationQueueMessageItem[] = [];
private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = { private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = {
unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender), unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender),
@ -431,6 +432,14 @@ export default class NotificationBackground {
this.removeTabFromNotificationQueue(tab); this.removeTabFromNotificationQueue(tab);
} }
/**
* Saves a cipher based on the message sent from the notification bar. If the vault
* is locked, the message will be added to the notification queue and the unlock
* popout will be opened.
*
* @param message - The extension message
* @param sender - The contextual sender of the message
*/
private async handleSaveCipherMessage( private async handleSaveCipherMessage(
message: NotificationBackgroundExtensionMessage, message: NotificationBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender, sender: chrome.runtime.MessageSender,
@ -454,6 +463,14 @@ export default class NotificationBackground {
await this.saveOrUpdateCredentials(sender.tab, message.edit, message.folder); await this.saveOrUpdateCredentials(sender.tab, message.edit, message.folder);
} }
/**
* Saves or updates credentials based on the message within the
* notification queue that is associated with the specified tab.
*
* @param tab - The tab to save or update credentials for
* @param edit - Identifies if the credentials should be edited or simply added
* @param folderId - The folder to add the cipher to
*/
private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, edit: boolean, folderId?: string) { private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, edit: boolean, folderId?: string) {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) { for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
const queueMessage = this.notificationQueue[i]; const queueMessage = this.notificationQueue[i];
@ -471,9 +488,6 @@ export default class NotificationBackground {
} }
this.notificationQueue.splice(i, 1); this.notificationQueue.splice(i, 1);
BrowserApi.tabSendMessageData(tab, "closeNotificationBar").catch((error) =>
this.logService.error(error),
);
if (queueMessage.type === NotificationQueueMessageType.ChangePassword) { if (queueMessage.type === NotificationQueueMessageType.ChangePassword) {
const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId); const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId);
@ -481,38 +495,52 @@ export default class NotificationBackground {
return; return;
} }
if (queueMessage.type === NotificationQueueMessageType.AddLogin) { // If the vault was locked, check if a cipher needs updating instead of creating a new one
// If the vault was locked, check if a cipher needs updating instead of creating a new one if (queueMessage.wasVaultLocked) {
if (queueMessage.wasVaultLocked) { const allCiphers = await this.cipherService.getAllDecryptedForUrl(queueMessage.uri);
const allCiphers = await this.cipherService.getAllDecryptedForUrl(queueMessage.uri); const existingCipher = allCiphers.find(
const existingCipher = allCiphers.find( (c) =>
(c) => c.login.username != null && c.login.username.toLowerCase() === queueMessage.username,
c.login.username != null && c.login.username.toLowerCase() === queueMessage.username, );
);
if (existingCipher != null) { if (existingCipher != null) {
await this.updatePassword(existingCipher, queueMessage.password, edit, tab); await this.updatePassword(existingCipher, queueMessage.password, edit, tab);
return;
}
}
folderId = (await this.folderExists(folderId)) ? folderId : null;
const newCipher = this.convertAddLoginQueueMessageToCipherView(queueMessage, folderId);
if (edit) {
await this.editItem(newCipher, tab);
return; return;
} }
}
const cipher = await this.cipherService.encrypt(newCipher); folderId = (await this.folderExists(folderId)) ? folderId : null;
const newCipher = this.convertAddLoginQueueMessageToCipherView(queueMessage, folderId);
if (edit) {
await this.editItem(newCipher, tab);
await BrowserApi.tabSendMessage(tab, { command: "closeNotificationBar" });
return;
}
const cipher = await this.cipherService.encrypt(newCipher);
try {
await this.cipherService.createWithServer(cipher); await this.cipherService.createWithServer(cipher);
BrowserApi.tabSendMessageData(tab, "addedCipher").catch((error) => await BrowserApi.tabSendMessage(tab, { command: "saveCipherAttemptCompleted" });
this.logService.error(error), await BrowserApi.tabSendMessage(tab, { command: "addedCipher" });
); } catch (error) {
await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", {
error: String(error.message),
});
} }
} }
} }
/**
* Handles updating an existing cipher's password. If the cipher
* is being edited, a popup will be opened to allow the user to
* edit the cipher.
*
* @param cipherView - The cipher to update
* @param newPassword - The new password to update the cipher with
* @param edit - Identifies if the cipher should be edited or simply updated
* @param tab - The tab that the message was sent from
*/
private async updatePassword( private async updatePassword(
cipherView: CipherView, cipherView: CipherView,
newPassword: string, newPassword: string,
@ -523,23 +551,37 @@ export default class NotificationBackground {
if (edit) { if (edit) {
await this.editItem(cipherView, tab); await this.editItem(cipherView, tab);
await BrowserApi.tabSendMessage(tab, { command: "closeNotificationBar" });
await BrowserApi.tabSendMessage(tab, { command: "editedCipher" }); await BrowserApi.tabSendMessage(tab, { command: "editedCipher" });
return; return;
} }
const cipher = await this.cipherService.encrypt(cipherView); const cipher = await this.cipherService.encrypt(cipherView);
await this.cipherService.updateWithServer(cipher); try {
// We've only updated the password, no need to broadcast editedCipher message // We've only updated the password, no need to broadcast editedCipher message
return; await this.cipherService.updateWithServer(cipher);
await BrowserApi.tabSendMessage(tab, { command: "saveCipherAttemptCompleted" });
} catch (error) {
await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", {
error: String(error.message),
});
}
} }
/**
* Sets the add/edit cipher info in the state service
* and opens the add/edit vault item popout.
*
* @param cipherView - The cipher to edit
* @param senderTab - The tab that the message was sent from
*/
private async editItem(cipherView: CipherView, senderTab: chrome.tabs.Tab) { private async editItem(cipherView: CipherView, senderTab: chrome.tabs.Tab) {
await this.stateService.setAddEditCipherInfo({ await this.stateService.setAddEditCipherInfo({
cipher: cipherView, cipher: cipherView,
collectionIds: cipherView.collectionIds, collectionIds: cipherView.collectionIds,
}); });
await openAddEditVaultItemPopout(senderTab, { cipherId: cipherView.id }); await this.openAddEditVaultItemPopout(senderTab, { cipherId: cipherView.id });
} }
private async folderExists(folderId: string) { private async folderExists(folderId: string) {

View File

@ -194,6 +194,18 @@ async function loadNotificationBar() {
watchForms(msg.data.forms); watchForms(msg.data.forms);
sendResponse(); sendResponse();
return true; return true;
} else if (msg.command === "saveCipherAttemptCompleted") {
if (!notificationBarIframe) {
return;
}
notificationBarIframe.contentWindow?.postMessage(
{
command: "saveCipherAttemptCompleted",
error: msg.data?.error,
},
"*",
);
} }
} }
// End Message Processing // End Message Processing

View File

@ -0,0 +1,13 @@
import { SaveOrUpdateCipherResult } from "../../background/abstractions/notification.background";
type NotificationBarWindowMessage = {
[key: string]: any;
command: string;
};
type NotificationBarWindowMessageHandlers = {
[key: string]: CallableFunction;
saveCipherAttemptCompleted: ({ message }: { message: SaveOrUpdateCipherResult }) => void;
};
export { NotificationBarWindowMessage, NotificationBarWindowMessageHandlers };

View File

@ -6,7 +6,7 @@
</head> </head>
<body> <body>
<div class="outer-wrapper"> <div id="notification-bar-outer-wrapper" class="outer-wrapper">
<div class="logo"> <div class="logo">
<a href="https://vault.bitwarden.com" target="_blank" id="logo-link" rel="noreferrer"> <a href="https://vault.bitwarden.com" target="_blank" id="logo-link" rel="noreferrer">
<img id="logo" alt="Bitwarden" /> <img id="logo" alt="Bitwarden" />
@ -33,7 +33,7 @@
<template id="template-add"> <template id="template-add">
<div class="inner-wrapper"> <div class="inner-wrapper">
<div id="add-text"></div> <div id="add-text"></div>
<div> <div class="add-change-cipher-buttons">
<button type="button" id="never-save" class="link"></button> <button type="button" id="never-save" class="link"></button>
<select id="select-folder"></select> <select id="select-folder"></select>
<button type="button" id="add-edit" class="secondary"></button> <button type="button" id="add-edit" class="secondary"></button>
@ -45,7 +45,7 @@
<template id="template-change"> <template id="template-change">
<div class="inner-wrapper"> <div class="inner-wrapper">
<div id="change-text"></div> <div id="change-text"></div>
<div> <div class="add-change-cipher-buttons">
<button type="button" id="change-edit" class="secondary"></button> <button type="button" id="change-edit" class="secondary"></button>
<button type="button" id="change-save" class="primary"></button> <button type="button" id="change-save" class="primary"></button>
</div> </div>

View File

@ -26,6 +26,18 @@ body {
@include themify($themes) { @include themify($themes) {
border-bottom-color: themed("primaryColor"); border-bottom-color: themed("primaryColor");
} }
&.success-event {
@include themify($themes) {
border-bottom-color: themed("successColor");
}
}
&.error-event {
@include themify($themes) {
border-bottom-color: themed("errorColor");
}
}
} }
.inner-wrapper { .inner-wrapper {

View File

@ -2,11 +2,23 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l
import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { FilelessImportPort, FilelessImportType } from "../../tools/enums/fileless-import.enums"; import { FilelessImportPort, FilelessImportType } from "../../tools/enums/fileless-import.enums";
import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background"; import {
AdjustNotificationBarMessageData,
SaveOrUpdateCipherResult,
} from "../background/abstractions/notification.background";
import {
NotificationBarWindowMessageHandlers,
NotificationBarWindowMessage,
} from "./abstractions/notification-bar";
require("./bar.scss"); require("./bar.scss");
const logService = new ConsoleLogService(false); const logService = new ConsoleLogService(false);
let windowMessageOrigin: string;
const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers = {
saveCipherAttemptCompleted: ({ message }) => handleSaveCipherAttemptCompletedMessage(message),
};
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
// delay 50ms so that we get proper body dimensions // delay 50ms so that we get proper body dimensions
@ -14,6 +26,8 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
function load() { function load() {
setupWindowMessageListener();
const theme = getQueryVariable("theme"); const theme = getQueryVariable("theme");
document.documentElement.classList.add("theme_" + theme); document.documentElement.classList.add("theme_" + theme);
@ -205,6 +219,30 @@ function sendSaveCipherMessage(edit: boolean, folder?: string) {
}); });
} }
function handleSaveCipherAttemptCompletedMessage(message: SaveOrUpdateCipherResult) {
const addSaveButtonContainers = document.querySelectorAll(".add-change-cipher-buttons");
const notificationBarOuterWrapper = document.getElementById("notification-bar-outer-wrapper");
if (message?.error) {
addSaveButtonContainers.forEach((element) => {
element.textContent = chrome.i18n.getMessage("saveCipherAttemptFailed");
element.classList.add("error-message");
notificationBarOuterWrapper.classList.add("error-event");
});
logService.error(`Error encountered when saving credentials: ${message.error}`);
return;
}
const messageName =
getQueryVariable("type") === "add" ? "saveCipherAttemptSuccess" : "updateCipherAttemptSuccess";
addSaveButtonContainers.forEach((element) => {
element.textContent = chrome.i18n.getMessage(messageName);
element.classList.add("success-message");
notificationBarOuterWrapper.classList.add("success-event");
});
setTimeout(() => sendPlatformMessage({ command: "bgCloseNotificationBar" }), 1250);
}
function handleTypeUnlock() { function handleTypeUnlock() {
setContent(document.getElementById("template-unlock") as HTMLTemplateElement); setContent(document.getElementById("template-unlock") as HTMLTemplateElement);
@ -248,17 +286,19 @@ function handleTypeFilelessImport() {
port.disconnect(); port.disconnect();
const filelessImportButtons = document.getElementById("fileless-import-buttons");
const notificationBarOuterWrapper = document.getElementById("notification-bar-outer-wrapper");
if (msg.command === "filelessImportCompleted") { if (msg.command === "filelessImportCompleted") {
document.getElementById("fileless-import-buttons").textContent = chrome.i18n.getMessage( filelessImportButtons.textContent = chrome.i18n.getMessage("dataSuccessfullyImported");
"dataSuccessfullyImported", filelessImportButtons.classList.add("success-message");
); notificationBarOuterWrapper.classList.add("success-event");
document.getElementById("fileless-import-buttons").classList.add("success-message");
return; return;
} }
document.getElementById("fileless-import-buttons").textContent = filelessImportButtons.textContent = chrome.i18n.getMessage("dataImportFailed");
chrome.i18n.getMessage("dataImportFailed"); filelessImportButtons.classList.add("error-message");
document.getElementById("fileless-import-buttons").classList.add("error-message"); notificationBarOuterWrapper.classList.add("error-event");
logService.error(`Error Encountered During Import: ${msg.importErrorMessage}`); logService.error(`Error Encountered During Import: ${msg.importErrorMessage}`);
}; };
port.onMessage.addListener(handlePortMessage); port.onMessage.addListener(handlePortMessage);
@ -321,3 +361,25 @@ function adjustHeight() {
data, data,
}); });
} }
function setupWindowMessageListener() {
globalThis.addEventListener("message", handleWindowMessage);
}
function handleWindowMessage(event: MessageEvent) {
if (!windowMessageOrigin) {
windowMessageOrigin = event.origin;
}
if (event.origin !== windowMessageOrigin) {
return;
}
const message = event.data as NotificationBarWindowMessage;
const handler = notificationBarWindowMessageHandlers[message.command];
if (!handler) {
return;
}
handler({ message });
}