mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-22 11:45:59 +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:
parent
43d1174a06
commit
3367339f9b
@ -2542,8 +2542,8 @@
|
||||
"description": "LastPass specific notification button text for cancelling a fileless import."
|
||||
},
|
||||
"startFilelessImport": {
|
||||
"message": "Import to Bitwarden",
|
||||
"description": "Notification button text for starting a fileless import."
|
||||
"message": "Import to Bitwarden",
|
||||
"description": "Notification button text for starting a fileless import."
|
||||
},
|
||||
"importing": {
|
||||
"message": "Importing...",
|
||||
@ -2993,5 +2993,17 @@
|
||||
"makeDefault": {
|
||||
"message": "Make default",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
@ -88,6 +88,8 @@ type NotificationBackgroundExtensionMessage = {
|
||||
notificationType?: string;
|
||||
};
|
||||
|
||||
type SaveOrUpdateCipherResult = undefined | { error: string };
|
||||
|
||||
type BackgroundMessageParam = { message: NotificationBackgroundExtensionMessage };
|
||||
type BackgroundSenderParam = { sender: chrome.runtime.MessageSender };
|
||||
type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam;
|
||||
@ -120,6 +122,7 @@ export {
|
||||
ChangePasswordMessageData,
|
||||
UnlockVaultMessageData,
|
||||
AddLoginMessageData,
|
||||
SaveOrUpdateCipherResult,
|
||||
NotificationBackgroundExtensionMessage,
|
||||
NotificationBackgroundExtensionMessageHandlers,
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import { createAutofillPageDetailsMock, createChromeTabMock } from "../spec/auto
|
||||
import { flushPromises, sendExtensionRuntimeMessage } from "../spec/testing-utils";
|
||||
|
||||
import {
|
||||
AddChangePasswordQueueMessage,
|
||||
AddLoginQueueMessage,
|
||||
AddUnlockVaultQueueMessage,
|
||||
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", () => {
|
||||
let tabSendMessageDataSpy: jest.SpyInstance;
|
||||
|
||||
|
@ -39,6 +39,7 @@ import { OverlayBackgroundExtensionMessage } from "./abstractions/overlay.backgr
|
||||
|
||||
export default class NotificationBackground {
|
||||
private openUnlockPopout = openUnlockPopout;
|
||||
private openAddEditVaultItemPopout = openAddEditVaultItemPopout;
|
||||
private notificationQueue: NotificationQueueMessageItem[] = [];
|
||||
private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = {
|
||||
unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender),
|
||||
@ -431,6 +432,14 @@ export default class NotificationBackground {
|
||||
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(
|
||||
message: NotificationBackgroundExtensionMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
@ -454,6 +463,14 @@ export default class NotificationBackground {
|
||||
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) {
|
||||
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
|
||||
const queueMessage = this.notificationQueue[i];
|
||||
@ -471,9 +488,6 @@ export default class NotificationBackground {
|
||||
}
|
||||
|
||||
this.notificationQueue.splice(i, 1);
|
||||
BrowserApi.tabSendMessageData(tab, "closeNotificationBar").catch((error) =>
|
||||
this.logService.error(error),
|
||||
);
|
||||
|
||||
if (queueMessage.type === NotificationQueueMessageType.ChangePassword) {
|
||||
const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId);
|
||||
@ -481,38 +495,52 @@ export default class NotificationBackground {
|
||||
return;
|
||||
}
|
||||
|
||||
if (queueMessage.type === NotificationQueueMessageType.AddLogin) {
|
||||
// If the vault was locked, check if a cipher needs updating instead of creating a new one
|
||||
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 the vault was locked, check if a cipher needs updating instead of creating a new one
|
||||
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 (existingCipher != null) {
|
||||
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);
|
||||
if (existingCipher != null) {
|
||||
await this.updatePassword(existingCipher, queueMessage.password, edit, tab);
|
||||
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);
|
||||
BrowserApi.tabSendMessageData(tab, "addedCipher").catch((error) =>
|
||||
this.logService.error(error),
|
||||
);
|
||||
await BrowserApi.tabSendMessage(tab, { command: "saveCipherAttemptCompleted" });
|
||||
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(
|
||||
cipherView: CipherView,
|
||||
newPassword: string,
|
||||
@ -523,23 +551,37 @@ export default class NotificationBackground {
|
||||
|
||||
if (edit) {
|
||||
await this.editItem(cipherView, tab);
|
||||
await BrowserApi.tabSendMessage(tab, { command: "closeNotificationBar" });
|
||||
await BrowserApi.tabSendMessage(tab, { command: "editedCipher" });
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
try {
|
||||
// We've only updated the password, no need to broadcast editedCipher message
|
||||
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) {
|
||||
await this.stateService.setAddEditCipherInfo({
|
||||
cipher: cipherView,
|
||||
collectionIds: cipherView.collectionIds,
|
||||
});
|
||||
|
||||
await openAddEditVaultItemPopout(senderTab, { cipherId: cipherView.id });
|
||||
await this.openAddEditVaultItemPopout(senderTab, { cipherId: cipherView.id });
|
||||
}
|
||||
|
||||
private async folderExists(folderId: string) {
|
||||
|
@ -194,6 +194,18 @@ async function loadNotificationBar() {
|
||||
watchForms(msg.data.forms);
|
||||
sendResponse();
|
||||
return true;
|
||||
} else if (msg.command === "saveCipherAttemptCompleted") {
|
||||
if (!notificationBarIframe) {
|
||||
return;
|
||||
}
|
||||
|
||||
notificationBarIframe.contentWindow?.postMessage(
|
||||
{
|
||||
command: "saveCipherAttemptCompleted",
|
||||
error: msg.data?.error,
|
||||
},
|
||||
"*",
|
||||
);
|
||||
}
|
||||
}
|
||||
// End Message Processing
|
||||
|
@ -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 };
|
@ -6,7 +6,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="outer-wrapper">
|
||||
<div id="notification-bar-outer-wrapper" class="outer-wrapper">
|
||||
<div class="logo">
|
||||
<a href="https://vault.bitwarden.com" target="_blank" id="logo-link" rel="noreferrer">
|
||||
<img id="logo" alt="Bitwarden" />
|
||||
@ -33,7 +33,7 @@
|
||||
<template id="template-add">
|
||||
<div class="inner-wrapper">
|
||||
<div id="add-text"></div>
|
||||
<div>
|
||||
<div class="add-change-cipher-buttons">
|
||||
<button type="button" id="never-save" class="link"></button>
|
||||
<select id="select-folder"></select>
|
||||
<button type="button" id="add-edit" class="secondary"></button>
|
||||
@ -45,7 +45,7 @@
|
||||
<template id="template-change">
|
||||
<div class="inner-wrapper">
|
||||
<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-save" class="primary"></button>
|
||||
</div>
|
||||
|
@ -26,6 +26,18 @@ body {
|
||||
@include themify($themes) {
|
||||
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 {
|
||||
|
@ -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 { 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");
|
||||
|
||||
const logService = new ConsoleLogService(false);
|
||||
let windowMessageOrigin: string;
|
||||
const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers = {
|
||||
saveCipherAttemptCompleted: ({ message }) => handleSaveCipherAttemptCompletedMessage(message),
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// delay 50ms so that we get proper body dimensions
|
||||
@ -14,6 +26,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
|
||||
function load() {
|
||||
setupWindowMessageListener();
|
||||
|
||||
const theme = getQueryVariable("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() {
|
||||
setContent(document.getElementById("template-unlock") as HTMLTemplateElement);
|
||||
|
||||
@ -248,17 +286,19 @@ function handleTypeFilelessImport() {
|
||||
|
||||
port.disconnect();
|
||||
|
||||
const filelessImportButtons = document.getElementById("fileless-import-buttons");
|
||||
const notificationBarOuterWrapper = document.getElementById("notification-bar-outer-wrapper");
|
||||
|
||||
if (msg.command === "filelessImportCompleted") {
|
||||
document.getElementById("fileless-import-buttons").textContent = chrome.i18n.getMessage(
|
||||
"dataSuccessfullyImported",
|
||||
);
|
||||
document.getElementById("fileless-import-buttons").classList.add("success-message");
|
||||
filelessImportButtons.textContent = chrome.i18n.getMessage("dataSuccessfullyImported");
|
||||
filelessImportButtons.classList.add("success-message");
|
||||
notificationBarOuterWrapper.classList.add("success-event");
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("fileless-import-buttons").textContent =
|
||||
chrome.i18n.getMessage("dataImportFailed");
|
||||
document.getElementById("fileless-import-buttons").classList.add("error-message");
|
||||
filelessImportButtons.textContent = chrome.i18n.getMessage("dataImportFailed");
|
||||
filelessImportButtons.classList.add("error-message");
|
||||
notificationBarOuterWrapper.classList.add("error-event");
|
||||
logService.error(`Error Encountered During Import: ${msg.importErrorMessage}`);
|
||||
};
|
||||
port.onMessage.addListener(handlePortMessage);
|
||||
@ -321,3 +361,25 @@ function adjustHeight() {
|
||||
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 });
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user