-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[PS-817] Add Generate Password Shortcut to MV3 (#3575)
* Add generate command * Add JSDoc * Minor improvements * Remove unneeded comment * Make some properties optional * Remove main.background.ts changes * One more * Lint * Make all but length optional * Address PR feedback * Move generate command code to command * Address PR feedback * Use new alarm scheme * Let feature handle state keys Moves to a feature folder and creates clipboard-module level state handler functions. StateService is being paired down to storage routing, so we are handling storage specifics in-module. Co-authored-by: Justin Baur <justindbaur@users.noreply.github.com> Co-authored-by: Daniel Smith <djsmith85@users.noreply.github.com> * Missed some changes Co-authored-by: Matt Gibson <mgibson@bitwarden.com> Co-authored-by: Justin Baur <justindbaur@users.noreply.github.com> Co-authored-by: Daniel Smith <djsmith85@users.noreply.github.com>
- Loading branch information
1 parent
cf2d3f5
commit 1d1986e
Showing
19 changed files
with
447 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
apps/browser/src/background/service_factories/password-generation-service.factory.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { PasswordGenerationService as AbstractPasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; | ||
import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service"; | ||
|
||
import { cryptoServiceFactory, CryptoServiceInitOptions } from "./crypto-service.factory"; | ||
import { CachedServices, factory, FactoryOptions } from "./factory-options"; | ||
import { policyServiceFactory, PolicyServiceInitOptions } from "./policy-service.factory"; | ||
import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; | ||
|
||
type PasswordGenerationServiceFactoryOptions = FactoryOptions; | ||
|
||
export type PasswordGenerationServiceInitOptions = PasswordGenerationServiceFactoryOptions & | ||
CryptoServiceInitOptions & | ||
PolicyServiceInitOptions & | ||
StateServiceInitOptions; | ||
|
||
export function passwordGenerationServiceFactory( | ||
cache: { passwordGenerationService?: AbstractPasswordGenerationService } & CachedServices, | ||
opts: PasswordGenerationServiceInitOptions | ||
): Promise<AbstractPasswordGenerationService> { | ||
return factory( | ||
cache, | ||
"passwordGenerationService", | ||
opts, | ||
async () => | ||
new PasswordGenerationService( | ||
await cryptoServiceFactory(cache, opts), | ||
await policyServiceFactory(cache, opts), | ||
await stateServiceFactory(cache, opts) | ||
) | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { mock, MockProxy } from "jest-mock-extended"; | ||
|
||
import { BrowserApi } from "../browser/browserApi"; | ||
import { StateService } from "../services/abstractions/state.service"; | ||
|
||
import { ClearClipboard } from "./clearClipboard"; | ||
import { getClearClipboardTime, setClearClipboardTime } from "./clipboard-state"; | ||
|
||
jest.mock("./clipboard-state", () => { | ||
return { | ||
getClearClipboardTime: jest.fn(), | ||
setClearClipboardTime: jest.fn(), | ||
}; | ||
}); | ||
|
||
const getClearClipboardTimeMock = getClearClipboardTime as jest.Mock; | ||
const setClearClipboardTimeMock = setClearClipboardTime as jest.Mock; | ||
|
||
describe("clearClipboard", () => { | ||
describe("run", () => { | ||
let stateService: MockProxy<StateService>; | ||
let serviceCache: Record<string, unknown>; | ||
|
||
beforeEach(() => { | ||
stateService = mock<StateService>(); | ||
serviceCache = { | ||
stateService: stateService, | ||
}; | ||
}); | ||
|
||
afterEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
it("has a clear time that is past execution time", async () => { | ||
const executionTime = new Date(2022, 1, 1, 12); | ||
const clearTime = new Date(2022, 1, 1, 12, 1); | ||
|
||
jest.spyOn(BrowserApi, "getActiveTabs").mockResolvedValue([ | ||
{ | ||
id: 1, | ||
}, | ||
] as any); | ||
|
||
jest.spyOn(BrowserApi, "sendTabsMessage").mockReturnValue(); | ||
|
||
getClearClipboardTimeMock.mockResolvedValue(clearTime.getTime()); | ||
|
||
await ClearClipboard.run(executionTime, serviceCache); | ||
|
||
expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledTimes(1); | ||
|
||
expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledWith(1, { | ||
command: "clearClipboard", | ||
}); | ||
}); | ||
|
||
it("has a clear time before execution time", async () => { | ||
const executionTime = new Date(2022, 1, 1, 12); | ||
const clearTime = new Date(2022, 1, 1, 11); | ||
|
||
setClearClipboardTimeMock.mockResolvedValue(clearTime.getTime()); | ||
|
||
await ClearClipboard.run(executionTime, serviceCache); | ||
|
||
expect(jest.spyOn(BrowserApi, "getActiveTabs")).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("has an undefined clearTime", async () => { | ||
const executionTime = new Date(2022, 1, 1); | ||
|
||
getClearClipboardTimeMock.mockResolvedValue(undefined); | ||
|
||
await ClearClipboard.run(executionTime, serviceCache); | ||
|
||
expect(jest.spyOn(BrowserApi, "getActiveTabs")).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { StateFactory } from "@bitwarden/common/factories/stateFactory"; | ||
import { GlobalState } from "@bitwarden/common/models/domain/global-state"; | ||
|
||
import { stateServiceFactory } from "../background/service_factories/state-service.factory"; | ||
import { BrowserApi } from "../browser/browserApi"; | ||
import { Account } from "../models/account"; | ||
|
||
import { getClearClipboardTime } from "./clipboard-state"; | ||
|
||
export class ClearClipboard { | ||
static async run(executionTime: Date, serviceCache: Record<string, unknown>) { | ||
const stateFactory = new StateFactory(GlobalState, Account); | ||
const stateService = await stateServiceFactory(serviceCache, { | ||
cryptoFunctionServiceOptions: { | ||
win: self, | ||
}, | ||
encryptServiceOptions: { | ||
logMacFailures: false, | ||
}, | ||
logServiceOptions: { | ||
isDev: false, | ||
}, | ||
stateMigrationServiceOptions: { | ||
stateFactory: stateFactory, | ||
}, | ||
stateServiceOptions: { | ||
stateFactory: stateFactory, | ||
}, | ||
}); | ||
|
||
const clearClipboardTime = await getClearClipboardTime(stateService); | ||
|
||
if (!clearClipboardTime) { | ||
return; | ||
} | ||
|
||
if (clearClipboardTime < executionTime.getTime()) { | ||
return; | ||
} | ||
|
||
const activeTabs = await BrowserApi.getActiveTabs(); | ||
if (!activeTabs || activeTabs.length === 0) { | ||
return; | ||
} | ||
|
||
BrowserApi.sendTabsMessage(activeTabs[0].id, { | ||
command: "clearClipboard", | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { StateService } from "../services/abstractions/state.service"; | ||
|
||
const clearClipboardStorageKey = "clearClipboardTime"; | ||
export const getClearClipboardTime = async (stateService: StateService) => { | ||
return await stateService.getFromSessionMemory<number>(clearClipboardStorageKey); | ||
}; | ||
|
||
export const setClearClipboardTime = async (stateService: StateService, time: number) => { | ||
await stateService.setInSessionMemory(clearClipboardStorageKey, time); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { BrowserApi } from "../browser/browserApi"; | ||
|
||
/** | ||
* Copies text to the clipboard in a MV3 safe way. | ||
* @param tab - The tab that the text will be sent to so that it can be copied to the users clipboard this needs to be an active tab or the DOM won't be able to be used to do the action. The tab sent in here should be from a user started action or queried for active tabs. | ||
* @param text - The text that you want added to the users clipboard. | ||
*/ | ||
export const copyToClipboard = async (tab: chrome.tabs.Tab, text: string) => { | ||
if (tab.id == null) { | ||
throw new Error("Cannot copy text to clipboard with a tab that does not have an id."); | ||
} | ||
|
||
BrowserApi.sendTabsMessage(tab.id, { | ||
command: "copyText", | ||
text: text, | ||
}); | ||
}; |
76 changes: 76 additions & 0 deletions
76
apps/browser/src/clipboard/generate-password-to-clipboard-command.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { mock, MockProxy } from "jest-mock-extended"; | ||
|
||
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; | ||
|
||
import { BrowserApi } from "../browser/browserApi"; | ||
import { StateService } from "../services/abstractions/state.service"; | ||
|
||
import { setClearClipboardTime } from "./clipboard-state"; | ||
import { GeneratePasswordToClipboardCommand } from "./generate-password-to-clipboard-command"; | ||
|
||
jest.mock("./clipboard-state", () => { | ||
return { | ||
getClearClipboardTime: jest.fn(), | ||
setClearClipboardTime: jest.fn(), | ||
}; | ||
}); | ||
|
||
const setClearClipboardTimeMock = setClearClipboardTime as jest.Mock; | ||
|
||
describe("GeneratePasswordToClipboardCommand", () => { | ||
let passwordGenerationService: MockProxy<PasswordGenerationService>; | ||
let stateService: MockProxy<StateService>; | ||
|
||
let sut: GeneratePasswordToClipboardCommand; | ||
|
||
beforeEach(() => { | ||
passwordGenerationService = mock<PasswordGenerationService>(); | ||
stateService = mock<StateService>(); | ||
|
||
passwordGenerationService.getOptions.mockResolvedValue([{ length: 8 }, {} as any]); | ||
|
||
passwordGenerationService.generatePassword.mockResolvedValue("PASSWORD"); | ||
|
||
jest.spyOn(BrowserApi, "sendTabsMessage").mockReturnValue(); | ||
|
||
sut = new GeneratePasswordToClipboardCommand(passwordGenerationService, stateService); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
describe("generatePasswordToClipboard", () => { | ||
it("has clear clipboard value", async () => { | ||
stateService.getClearClipboard.mockResolvedValue(5 * 60); // 5 minutes | ||
|
||
await sut.generatePasswordToClipboard({ id: 1 } as any); | ||
|
||
expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledTimes(1); | ||
|
||
expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledWith(1, { | ||
command: "copyText", | ||
text: "PASSWORD", | ||
}); | ||
|
||
expect(setClearClipboardTimeMock).toHaveBeenCalledTimes(1); | ||
|
||
expect(setClearClipboardTimeMock).toHaveBeenCalledWith(stateService, expect.any(Number)); | ||
}); | ||
|
||
it("does not have clear clipboard value", async () => { | ||
stateService.getClearClipboard.mockResolvedValue(null); | ||
|
||
await sut.generatePasswordToClipboard({ id: 1 } as any); | ||
|
||
expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledTimes(1); | ||
|
||
expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledWith(1, { | ||
command: "copyText", | ||
text: "PASSWORD", | ||
}); | ||
|
||
expect(setClearClipboardTimeMock).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
26 changes: 26 additions & 0 deletions
26
apps/browser/src/clipboard/generate-password-to-clipboard-command.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; | ||
|
||
import { StateService } from "../services/abstractions/state.service"; | ||
|
||
import { setClearClipboardTime } from "./clipboard-state"; | ||
import { copyToClipboard } from "./copy-to-clipboard-command"; | ||
|
||
export class GeneratePasswordToClipboardCommand { | ||
constructor( | ||
private passwordGenerationService: PasswordGenerationService, | ||
private stateService: StateService | ||
) {} | ||
|
||
async generatePasswordToClipboard(tab: chrome.tabs.Tab) { | ||
const [options] = await this.passwordGenerationService.getOptions(); | ||
const password = await this.passwordGenerationService.generatePassword(options); | ||
|
||
copyToClipboard(tab, password); | ||
|
||
const clearClipboard = await this.stateService.getClearClipboard(); | ||
|
||
if (clearClipboard != null) { | ||
await setClearClipboardTime(this.stateService, Date.now() + clearClipboard * 1000); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from "./clearClipboard"; | ||
export * from "./copy-to-clipboard-command"; | ||
export * from "./generate-password-to-clipboard-command"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { TabMessage } from "../types/tab-messages"; | ||
|
||
async function copyText(text: string) { | ||
await window.navigator.clipboard.writeText(text); | ||
} | ||
|
||
async function onMessageListener( | ||
msg: TabMessage, | ||
sender: chrome.runtime.MessageSender, | ||
responseCallback: (response: unknown) => void | ||
) { | ||
switch (msg.command) { | ||
case "copyText": | ||
await copyText(msg.text); | ||
break; | ||
case "clearClipboard": | ||
await copyText("\u0000"); | ||
break; | ||
default: | ||
} | ||
} | ||
|
||
chrome.runtime.onMessage.addListener(onMessageListener); |
Oops, something went wrong.