1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-08-27 23:31:41 +02:00

[PM-6426] Implementing clear clipboard call on generatePasswordToClipboard with the TaskSchedulerService

This commit is contained in:
Cesar Gonzalez 2024-04-01 11:45:16 -05:00
parent d989dc3804
commit 2f517336db
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
10 changed files with 65 additions and 156 deletions

View File

@ -36,6 +36,7 @@ import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
import { autofillSettingsServiceFactory } from "../../autofill/background/service_factories/autofill-settings-service.factory";
import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory";
import { Account } from "../../models/account";
import { browserTaskSchedulerServiceFactory } from "../../platform/background/service-factories/browser-task-scheduler-service.factory";
import { CachedServices } from "../../platform/background/service-factories/factory-options";
import { stateServiceFactory } from "../../platform/background/service-factories/state-service.factory";
import { BrowserApi } from "../../platform/browser/browser-api";
@ -116,6 +117,7 @@ export class ContextMenuClickedHandler {
const generatePasswordToClipboardCommand = new GeneratePasswordToClipboardCommand(
await passwordGenerationServiceFactory(cachedServices, serviceOptions),
await autofillSettingsServiceFactory(cachedServices, serviceOptions),
await browserTaskSchedulerServiceFactory(cachedServices, serviceOptions),
);
const autofillCommand = new AutofillTabCommand(

View File

@ -1,11 +1,9 @@
import { BrowserApi } from "../../platform/browser/browser-api";
export const clearClipboardAlarmName = "clearClipboard";
export class ClearClipboard {
/**
We currently rely on an active tab with an injected content script (`../content/misc-utils.ts`) to clear the clipboard via `window.navigator.clipboard.writeText(text)`
With https://bugs.chromium.org/p/chromium/issues/detail?id=1160302 it was said that service workers,
would have access to the clipboard api and then we could migrate to a simpler solution
*/

View File

@ -1,30 +1,24 @@
import { mock, MockProxy } from "jest-mock-extended";
import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { ScheduledTaskNames } from "@bitwarden/common/platform/enums/scheduled-task-name.enum";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { setAlarmTime } from "../../platform/alarms/alarm-state";
import { BrowserApi } from "../../platform/browser/browser-api";
import { BrowserTaskSchedulerService } from "../../platform/services/browser-task-scheduler.service";
import { clearClipboardAlarmName } from "./clear-clipboard";
import { GeneratePasswordToClipboardCommand } from "./generate-password-to-clipboard-command";
jest.mock("../../platform/alarms/alarm-state", () => {
return {
setAlarmTime: jest.fn(),
};
});
const setAlarmTimeMock = setAlarmTime as jest.Mock;
describe("GeneratePasswordToClipboardCommand", () => {
let passwordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
let autofillSettingsService: MockProxy<AutofillSettingsService>;
let browserTaskSchedulerService: MockProxy<BrowserTaskSchedulerService>;
let sut: GeneratePasswordToClipboardCommand;
beforeEach(() => {
passwordGenerationService = mock<PasswordGenerationServiceAbstraction>();
browserTaskSchedulerService = mock<BrowserTaskSchedulerService>();
passwordGenerationService.getOptions.mockResolvedValue([{ length: 8 }, {} as any]);
@ -35,6 +29,7 @@ describe("GeneratePasswordToClipboardCommand", () => {
sut = new GeneratePasswordToClipboardCommand(
passwordGenerationService,
autofillSettingsService,
browserTaskSchedulerService,
);
});
@ -55,9 +50,12 @@ describe("GeneratePasswordToClipboardCommand", () => {
text: "PASSWORD",
});
expect(setAlarmTimeMock).toHaveBeenCalledTimes(1);
expect(setAlarmTimeMock).toHaveBeenCalledWith(clearClipboardAlarmName, expect.any(Number));
expect(browserTaskSchedulerService.setTimeout).toHaveBeenCalledTimes(1);
expect(browserTaskSchedulerService.setTimeout).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Number),
ScheduledTaskNames.clearClipboardTimeout,
);
});
it("does not have clear clipboard value", async () => {
@ -71,8 +69,7 @@ describe("GeneratePasswordToClipboardCommand", () => {
command: "copyText",
text: "PASSWORD",
});
expect(setAlarmTimeMock).not.toHaveBeenCalled();
expect(browserTaskSchedulerService.setTimeout).not.toHaveBeenCalled();
});
});
});

View File

@ -1,17 +1,21 @@
import { firstValueFrom } from "rxjs";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { ScheduledTaskNames } from "@bitwarden/common/platform/enums/scheduled-task-name.enum";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { setAlarmTime } from "../../platform/alarms/alarm-state";
import { BrowserTaskSchedulerService } from "../../platform/services/abstractions/browser-task-scheduler.service";
import { clearClipboardAlarmName } from "./clear-clipboard";
import { ClearClipboard } from "./clear-clipboard";
import { copyToClipboard } from "./copy-to-clipboard-command";
export class GeneratePasswordToClipboardCommand {
private clearClipboardTimeout: number | NodeJS.Timeout;
constructor(
private passwordGenerationService: PasswordGenerationServiceAbstraction,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private taskSchedulerService: BrowserTaskSchedulerService,
) {}
async getClearClipboard() {
@ -22,14 +26,22 @@ export class GeneratePasswordToClipboardCommand {
const [options] = await this.passwordGenerationService.getOptions();
const password = await this.passwordGenerationService.generatePassword(options);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
copyToClipboard(tab, password);
await copyToClipboard(tab, password);
const clearClipboard = await this.getClearClipboard();
if (clearClipboard != null) {
await setAlarmTime(clearClipboardAlarmName, clearClipboard * 1000);
const clearClipboardDelayInSeconds = await this.getClearClipboard();
if (!clearClipboardDelayInSeconds) {
return;
}
const timeoutInMs = clearClipboardDelayInSeconds * 1000;
await this.taskSchedulerService.clearScheduledTask({
taskName: ScheduledTaskNames.clearClipboardTimeout,
timeoutId: this.clearClipboardTimeout,
});
await this.taskSchedulerService.setTimeout(
() => ClearClipboard.run(),
timeoutInMs,
ScheduledTaskNames.clearClipboardTimeout,
);
}
}

View File

@ -1,66 +0,0 @@
import { clearClipboardAlarmName } from "../../autofill/clipboard";
import { BrowserApi } from "../browser/browser-api";
export const alarmKeys = [clearClipboardAlarmName] as const;
export type AlarmKeys = (typeof alarmKeys)[number];
type AlarmState = { [T in AlarmKeys]: number | undefined };
const alarmState: AlarmState = {
clearClipboard: null,
//TODO once implemented vaultTimeout: null;
//TODO once implemented checkNotifications: null;
//TODO once implemented (if necessary) processReload: null;
};
/**
* Retrieves the set alarm time (planned execution) for a give an commandName {@link AlarmState}
* @param commandName A command that has been previously registered with {@link AlarmState}
* @returns {Promise<number>} null or Unix epoch timestamp when the alarm action is supposed to execute
* @example
* // getAlarmTime(clearClipboard)
*/
export async function getAlarmTime(commandName: AlarmKeys): Promise<number> {
let alarmTime: number;
if (BrowserApi.isManifestVersion(3)) {
const fromSessionStore = await chrome.storage.session.get(commandName);
alarmTime = fromSessionStore[commandName];
} else {
alarmTime = alarmState[commandName];
}
return alarmTime;
}
/**
* Registers an action that should execute after the given time has passed
* @param commandName A command that has been previously registered with {@link AlarmState}
* @param delay_ms The number of ms from now in which the command should execute from
* @example
* // setAlarmTime(clearClipboard, 5000) register the clearClipboard action which will execute when at least 5 seconds from now have passed
*/
export async function setAlarmTime(commandName: AlarmKeys, delay_ms: number): Promise<void> {
if (!delay_ms || delay_ms === 0) {
await this.clearAlarmTime(commandName);
return;
}
const time = Date.now() + delay_ms;
await setAlarmTimeInternal(commandName, time);
}
/**
* Clears the time currently set for a given command
* @param commandName A command that has been previously registered with {@link AlarmState}
*/
export async function clearAlarmTime(commandName: AlarmKeys): Promise<void> {
await setAlarmTimeInternal(commandName, null);
}
async function setAlarmTimeInternal(commandName: AlarmKeys, time: number): Promise<void> {
if (BrowserApi.isManifestVersion(3)) {
await chrome.storage.session.set({ [commandName]: time });
} else {
alarmState[commandName] = time;
}
}

View File

@ -1,28 +0,0 @@
import { ClearClipboard, clearClipboardAlarmName } from "../../autofill/clipboard";
import { alarmKeys, clearAlarmTime, getAlarmTime } from "./alarm-state";
export const onAlarmListener = async (alarm: chrome.alarms.Alarm) => {
alarmKeys.forEach(async (key) => {
const executionTime = await getAlarmTime(key);
if (!executionTime) {
return;
}
const currentDate = Date.now();
if (executionTime > currentDate) {
return;
}
await clearAlarmTime(key);
switch (key) {
case clearClipboardAlarmName:
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
ClearClipboard.run();
break;
default:
}
});
};

View File

@ -1,31 +0,0 @@
const NUMBER_OF_ALARMS = 6;
export function registerAlarms() {
alarmsToBeCreated(NUMBER_OF_ALARMS);
}
/**
* Creates staggered alarms that periodically (1min) raise OnAlarm events. The staggering is calculated based on the number of alarms passed in.
* @param numberOfAlarms Number of named alarms, that shall be registered
* @example
* // alarmsToBeCreated(2) results in 2 alarms separated by 30 seconds
* @example
* // alarmsToBeCreated(4) results in 4 alarms separated by 15 seconds
* @example
* // alarmsToBeCreated(6) results in 6 alarms separated by 10 seconds
* @example
* // alarmsToBeCreated(60) results in 60 alarms separated by 1 second
*/
function alarmsToBeCreated(numberOfAlarms: number): void {
const oneMinuteInMs = 60 * 1000;
const offset = oneMinuteInMs / numberOfAlarms;
let calculatedWhen: number = Date.now() + offset;
for (let index = 0; index < numberOfAlarms; index++) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
chrome.alarms.create(`bw_alarm${index}`, { periodInMinutes: 1, when: calculatedWhen });
calculatedWhen += offset;
}
}

View File

@ -1,7 +1,5 @@
import MainBackground from "../background/main.background";
import { onAlarmListener } from "./alarms/on-alarm-listener";
import { registerAlarms } from "./alarms/register-alarms";
import { BrowserApi } from "./browser/browser-api";
import {
contextMenusClickedListener,
@ -17,8 +15,6 @@ import {
if (BrowserApi.isManifestVersion(3)) {
chrome.commands.onCommand.addListener(onCommandListener);
chrome.runtime.onInstalled.addListener(onInstallListener);
chrome.alarms.onAlarm.addListener(onAlarmListener);
registerAlarms();
chrome.windows.onFocusChanged.addListener(windowsOnFocusChangedListener);
chrome.tabs.onActivated.addListener(tabsOnActivatedListener);
chrome.tabs.onReplaced.addListener(tabsOnReplacedListener);

View File

@ -0,0 +1,27 @@
import { BrowserTaskSchedulerService } from "../../services/browser-task-scheduler.service";
import { CachedServices, factory, FactoryOptions } from "./factory-options";
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
import { stateProviderFactory, StateProviderInitOptions } from "./state-provider.factory";
type BrowserTaskSchedulerServiceFactoryOptions = FactoryOptions;
export type BrowserTaskSchedulerServiceInitOptions = BrowserTaskSchedulerServiceFactoryOptions &
LogServiceInitOptions &
StateProviderInitOptions;
export function browserTaskSchedulerServiceFactory(
cache: { browserTaskSchedulerService?: BrowserTaskSchedulerService } & CachedServices,
opts: BrowserTaskSchedulerServiceInitOptions,
): Promise<BrowserTaskSchedulerService> {
return factory(
cache,
"browserTaskSchedulerService",
opts,
async () =>
new BrowserTaskSchedulerService(
await logServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
),
);
}

View File

@ -12,6 +12,7 @@ import {
passwordGenerationServiceFactory,
PasswordGenerationServiceInitOptions,
} from "../../tools/background/service_factories/password-generation-service.factory";
import { browserTaskSchedulerServiceFactory } from "../background/service-factories/browser-task-scheduler-service.factory";
import { CachedServices } from "../background/service-factories/factory-options";
import { logServiceFactory } from "../background/service-factories/log-service.factory";
import { BrowserApi } from "../browser/browser-api";
@ -102,6 +103,7 @@ const doGeneratePasswordToClipboard = async (tab: chrome.tabs.Tab): Promise<void
const command = new GeneratePasswordToClipboardCommand(
await passwordGenerationServiceFactory(cache, options),
await autofillSettingsServiceFactory(cache, options),
await browserTaskSchedulerServiceFactory(cache, options),
);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises