mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
Generate password triggers notifications (#15874)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { CollectionView } from "../../content/components/common-types";
|
||||
@@ -17,7 +18,7 @@ interface NotificationQueueMessage {
|
||||
|
||||
interface AddChangePasswordQueueMessage extends NotificationQueueMessage {
|
||||
type: "change";
|
||||
cipherId: string;
|
||||
cipherId: CipherView["id"];
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,19 +28,12 @@ export type ModifyLoginCipherFormData = {
|
||||
newPassword: string;
|
||||
};
|
||||
|
||||
export type ModifyLoginCipherFormDataForTab = Map<
|
||||
chrome.tabs.Tab["id"],
|
||||
{ uri: string; username: string; password: string; newPassword: string }
|
||||
>;
|
||||
export type ModifyLoginCipherFormDataForTab = Map<chrome.tabs.Tab["id"], ModifyLoginCipherFormData>;
|
||||
|
||||
export type OverlayNotificationsExtensionMessage = {
|
||||
command: string;
|
||||
uri?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
newPassword?: string;
|
||||
details?: AutofillPageDetails;
|
||||
};
|
||||
} & ModifyLoginCipherFormData;
|
||||
|
||||
type OverlayNotificationsMessageParams = { message: OverlayNotificationsExtensionMessage };
|
||||
type OverlayNotificationSenderParams = { sender: chrome.runtime.MessageSender };
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
|
||||
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { EnvironmentServerConfigData } from "@bitwarden/common/platform/models/data/server-config.data";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import AutofillField from "../models/autofill-field";
|
||||
@@ -27,9 +24,6 @@ import { OverlayNotificationsBackground } from "./overlay-notifications.backgrou
|
||||
describe("OverlayNotificationsBackground", () => {
|
||||
let logService: MockProxy<LogService>;
|
||||
let notificationBackground: NotificationBackground;
|
||||
let taskService: TaskService;
|
||||
let accountService: AccountService;
|
||||
let cipherService: CipherService;
|
||||
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
|
||||
let getEnableAddedLoginPromptSpy: jest.SpyInstance;
|
||||
let overlayNotificationsBackground: OverlayNotificationsBackground;
|
||||
@@ -38,9 +32,6 @@ describe("OverlayNotificationsBackground", () => {
|
||||
jest.useFakeTimers();
|
||||
logService = mock<LogService>();
|
||||
notificationBackground = mock<NotificationBackground>();
|
||||
taskService = mock<TaskService>();
|
||||
accountService = mock<AccountService>();
|
||||
cipherService = mock<CipherService>();
|
||||
getEnableChangedPasswordPromptSpy = jest
|
||||
.spyOn(notificationBackground, "getEnableChangedPasswordPrompt")
|
||||
.mockResolvedValue(true);
|
||||
@@ -50,9 +41,6 @@ describe("OverlayNotificationsBackground", () => {
|
||||
overlayNotificationsBackground = new OverlayNotificationsBackground(
|
||||
logService,
|
||||
notificationBackground,
|
||||
taskService,
|
||||
accountService,
|
||||
cipherService,
|
||||
);
|
||||
await overlayNotificationsBackground.init();
|
||||
});
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
// @ts-strict-ignore
|
||||
import { Subject, switchMap, timer } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { NotificationType, NotificationTypes } from "../notification/abstractions/notification-bar";
|
||||
@@ -31,6 +28,8 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
private notificationFallbackTimeout: number | NodeJS.Timeout | null;
|
||||
private readonly formSubmissionRequestMethods: Set<string> = new Set(["POST", "PUT", "PATCH"]);
|
||||
private readonly extensionMessageHandlers: OverlayNotificationsExtensionMessageHandlers = {
|
||||
generatedPasswordFilled: ({ message, sender }) =>
|
||||
this.storeModifiedLoginFormData(message, sender),
|
||||
formFieldSubmitted: ({ message, sender }) => this.storeModifiedLoginFormData(message, sender),
|
||||
collectPageDetailsResponse: ({ message, sender }) =>
|
||||
this.handleCollectPageDetailsResponse(message, sender),
|
||||
@@ -39,9 +38,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private notificationBackground: NotificationBackground,
|
||||
private taskService: TaskService,
|
||||
private accountService: AccountService,
|
||||
private cipherService: CipherService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -442,7 +438,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
}
|
||||
}
|
||||
|
||||
this.clearCompletedWebRequest(requestId, tab);
|
||||
this.clearCompletedWebRequest(requestId, tab.id);
|
||||
return results.join(" ");
|
||||
};
|
||||
|
||||
@@ -482,11 +478,11 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
*/
|
||||
private clearCompletedWebRequest = (
|
||||
requestId: chrome.webRequest.ResourceRequest["requestId"],
|
||||
tab: chrome.tabs.Tab,
|
||||
tabId: chrome.tabs.Tab["id"],
|
||||
) => {
|
||||
this.activeFormSubmissionRequests.delete(requestId);
|
||||
this.modifyLoginCipherFormData.delete(tab.id);
|
||||
this.websiteOriginsWithFields.delete(tab.id);
|
||||
this.modifyLoginCipherFormData.delete(tabId);
|
||||
this.websiteOriginsWithFields.delete(tabId);
|
||||
this.setupWebRequestsListeners();
|
||||
};
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ import {
|
||||
MAX_SUB_FRAME_DEPTH,
|
||||
RedirectFocusDirection,
|
||||
} from "../enums/autofill-overlay.enum";
|
||||
import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overlay-content.service";
|
||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||
import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service";
|
||||
import {
|
||||
@@ -71,6 +70,7 @@ import {
|
||||
triggerWebRequestOnCompletedEvent,
|
||||
} from "../spec/testing-utils";
|
||||
|
||||
import { ModifyLoginCipherFormData } from "./abstractions/overlay-notifications.background";
|
||||
import {
|
||||
FocusedFieldData,
|
||||
InlineMenuPosition,
|
||||
@@ -2076,7 +2076,7 @@ describe("OverlayBackground", () => {
|
||||
const tab = createChromeTabMock({ id: 2 });
|
||||
const sender = mock<chrome.runtime.MessageSender>({ tab, frameId: 100 });
|
||||
let focusedFieldData: FocusedFieldData;
|
||||
let formData: InlineMenuFormFieldData;
|
||||
let formData: ModifyLoginCipherFormData;
|
||||
|
||||
beforeEach(async () => {
|
||||
await initOverlayElementPorts();
|
||||
@@ -3651,6 +3651,18 @@ describe("OverlayBackground", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sends a message to the tab to store modify login change when a password is generated", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey });
|
||||
|
||||
await flushPromises();
|
||||
jest.advanceTimersByTime(400);
|
||||
await flushPromises();
|
||||
|
||||
expect(tabsSendMessageSpy.mock.lastCall[1].command).toBe("generatedPasswordModifyLogin");
|
||||
});
|
||||
|
||||
it("filters the page details to only include the new password fields before filling", async () => {
|
||||
sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey });
|
||||
await flushPromises();
|
||||
@@ -3663,31 +3675,6 @@ describe("OverlayBackground", () => {
|
||||
allowTotpAutofill: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("opens the inline menu for fields that fill a generated password", async () => {
|
||||
jest.useFakeTimers();
|
||||
const formData = {
|
||||
uri: "https://example.com",
|
||||
username: "username",
|
||||
password: "password",
|
||||
newPassword: "newPassword",
|
||||
};
|
||||
tabsSendMessageSpy.mockImplementation((_tab, message) => {
|
||||
if (message.command === "getInlineMenuFormFieldData") {
|
||||
return Promise.resolve(formData);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
const openInlineMenuSpy = jest.spyOn(overlayBackground as any, "openInlineMenu");
|
||||
|
||||
sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey });
|
||||
await flushPromises();
|
||||
jest.advanceTimersByTime(400);
|
||||
await flushPromises();
|
||||
|
||||
expect(openInlineMenuSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ import {
|
||||
MAX_SUB_FRAME_DEPTH,
|
||||
} from "../enums/autofill-overlay.enum";
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overlay-content.service";
|
||||
import { AutofillService, PageDetail } from "../services/abstractions/autofill.service";
|
||||
import { InlineMenuFieldQualificationService } from "../services/abstractions/inline-menu-field-qualifications.service";
|
||||
import {
|
||||
@@ -82,6 +81,7 @@ import {
|
||||
} from "../utils";
|
||||
|
||||
import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background";
|
||||
import { ModifyLoginCipherFormData } from "./abstractions/overlay-notifications.background";
|
||||
import {
|
||||
BuildCipherDataParams,
|
||||
CloseInlineMenuMessage,
|
||||
@@ -1813,7 +1813,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
|
||||
/**
|
||||
* Triggers a fill of the generated password into the current tab. Will trigger
|
||||
* a focus of the last focused field after filling the password.
|
||||
* a focus of the last focused field after filling the password.
|
||||
*
|
||||
* @param port - The port of the sender
|
||||
*/
|
||||
@@ -1857,10 +1857,16 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
});
|
||||
|
||||
globalThis.setTimeout(async () => {
|
||||
if (await this.shouldShowSaveLoginInlineMenuList(port.sender.tab)) {
|
||||
await this.openInlineMenu(port.sender, true);
|
||||
}
|
||||
}, 300);
|
||||
await BrowserApi.tabSendMessage(
|
||||
port.sender.tab,
|
||||
{
|
||||
command: "generatedPasswordModifyLogin",
|
||||
},
|
||||
{
|
||||
frameId: this.focusedFieldData.frameId || 0,
|
||||
},
|
||||
);
|
||||
}, 150);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1891,7 +1897,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
*
|
||||
* @param tab - The tab to get the form field data from
|
||||
*/
|
||||
private async getInlineMenuFormFieldData(tab: chrome.tabs.Tab): Promise<InlineMenuFormFieldData> {
|
||||
private async getInlineMenuFormFieldData(
|
||||
tab: chrome.tabs.Tab,
|
||||
): Promise<ModifyLoginCipherFormData> {
|
||||
return await BrowserApi.tabSendMessage(
|
||||
tab,
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ModifyLoginCipherFormData } from "../../background/abstractions/overlay-notifications.background";
|
||||
import { SubFrameOffsetData } from "../../background/abstractions/overlay.background";
|
||||
import { AutofillExtensionMessageParam } from "../../content/abstractions/autofill-init";
|
||||
import AutofillField from "../../models/autofill-field";
|
||||
@@ -8,13 +9,6 @@ export type SubFrameDataFromWindowMessage = SubFrameOffsetData & {
|
||||
subFrameDepth: number;
|
||||
};
|
||||
|
||||
export type InlineMenuFormFieldData = {
|
||||
uri: string;
|
||||
username: string;
|
||||
password: string;
|
||||
newPassword: string;
|
||||
};
|
||||
|
||||
export type AutofillOverlayContentExtensionMessageHandlers = {
|
||||
[key: string]: CallableFunction;
|
||||
addNewVaultItemFromOverlay: ({ message }: AutofillExtensionMessageParam) => void;
|
||||
@@ -32,7 +26,7 @@ export type AutofillOverlayContentExtensionMessageHandlers = {
|
||||
destroyAutofillInlineMenuListeners: () => void;
|
||||
getInlineMenuFormFieldData: ({
|
||||
message,
|
||||
}: AutofillExtensionMessageParam) => Promise<InlineMenuFormFieldData>;
|
||||
}: AutofillExtensionMessageParam) => Promise<ModifyLoginCipherFormData>;
|
||||
};
|
||||
|
||||
export interface AutofillOverlayContentService {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { ModifyLoginCipherFormData } from "../background/abstractions/overlay-notifications.background";
|
||||
import AutofillInit from "../content/autofill-init";
|
||||
import {
|
||||
AutofillOverlayElement,
|
||||
@@ -1750,6 +1751,29 @@ describe("AutofillOverlayContentService", () => {
|
||||
});
|
||||
|
||||
describe("extension onMessage handlers", () => {
|
||||
describe("generatedPasswordModifyLogin", () => {
|
||||
it("relays a message regarding password generation to store modified login data", async () => {
|
||||
const formFieldData: ModifyLoginCipherFormData = {
|
||||
newPassword: "newPassword",
|
||||
password: "password",
|
||||
uri: "http://localhost/",
|
||||
username: "username",
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(autofillOverlayContentService as any, "getFormFieldData")
|
||||
.mockResolvedValue(formFieldData);
|
||||
|
||||
sendMockExtensionMessage({
|
||||
command: "generatedPasswordModifyLogin",
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const resolvedValue = await sendExtensionMessageSpy.mock.calls[0][1];
|
||||
expect(resolvedValue).toEqual(formFieldData);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addNewVaultItemFromOverlay message handler", () => {
|
||||
it("skips sending the message if the overlay list is not visible", async () => {
|
||||
jest
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "@bitwarden/common/autofill/constants";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { ModifyLoginCipherFormData } from "../background/abstractions/overlay-notifications.background";
|
||||
import {
|
||||
FocusedFieldData,
|
||||
NewCardCipherData,
|
||||
@@ -48,7 +49,6 @@ import {
|
||||
import {
|
||||
AutofillOverlayContentExtensionMessageHandlers,
|
||||
AutofillOverlayContentService as AutofillOverlayContentServiceInterface,
|
||||
InlineMenuFormFieldData,
|
||||
SubFrameDataFromWindowMessage,
|
||||
} from "./abstractions/autofill-overlay-content.service";
|
||||
import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service";
|
||||
@@ -95,6 +95,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
destroyAutofillInlineMenuListeners: () => this.destroy(),
|
||||
getInlineMenuFormFieldData: ({ message }) =>
|
||||
this.handleGetInlineMenuFormFieldDataMessage(message),
|
||||
generatedPasswordModifyLogin: () => this.sendGeneratedPasswordModifyLogin(),
|
||||
};
|
||||
private readonly loginFieldQualifiers: Record<string, CallableFunction> = {
|
||||
[AutofillFieldQualifier.username]: this.inlineMenuFieldQualificationService.isUsernameField,
|
||||
@@ -235,6 +236,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On password generation, send form field data i.e. modified login data
|
||||
*/
|
||||
sendGeneratedPasswordModifyLogin = async () => {
|
||||
await this.sendExtensionMessage("generatedPasswordFilled", this.getFormFieldData());
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats any found user filled fields for a login cipher and sends a message
|
||||
* to the background script to add a new cipher.
|
||||
@@ -637,7 +645,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
/**
|
||||
* Returns the form field data used for add login and change password notifications.
|
||||
*/
|
||||
private getFormFieldData = (): InlineMenuFormFieldData => {
|
||||
private getFormFieldData = (): ModifyLoginCipherFormData => {
|
||||
return {
|
||||
uri: globalThis.document.URL,
|
||||
username: this.userFilledFields["username"]?.value || "",
|
||||
|
||||
@@ -1258,9 +1258,6 @@ export default class MainBackground {
|
||||
this.overlayNotificationsBackground = new OverlayNotificationsBackground(
|
||||
this.logService,
|
||||
this.notificationBackground,
|
||||
this.taskService,
|
||||
this.accountService,
|
||||
this.cipherService,
|
||||
);
|
||||
|
||||
this.autoSubmitLoginBackground = new AutoSubmitLoginBackground(
|
||||
|
||||
Reference in New Issue
Block a user