bitwarden-estensione-browser/apps/browser/src/autofill/browser/context-menu-clicked-handle...

239 lines
9.1 KiB
TypeScript

import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EventType } from "@bitwarden/common/enums";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
authServiceFactory,
AuthServiceInitOptions,
} from "../../auth/background/service-factories/auth-service.factory";
import { totpServiceFactory } from "../../auth/background/service-factories/totp-service.factory";
import LockedVaultPendingNotificationsItem from "../../background/models/lockedVaultPendingNotificationsItem";
import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory";
import { passwordGenerationServiceFactory } from "../../background/service-factories/password-generation-service.factory";
import { Account } from "../../models/account";
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";
import {
cipherServiceFactory,
CipherServiceInitOptions,
} from "../../vault/background/service_factories/cipher-service.factory";
import { autofillServiceFactory } from "../background/service_factories/autofill-service.factory";
import { copyToClipboard, GeneratePasswordToClipboardCommand } from "../clipboard";
import { AutofillTabCommand } from "../commands/autofill-tab-command";
import {
AUTOFILL_ID,
COPY_IDENTIFIER_ID,
COPY_PASSWORD_ID,
COPY_USERNAME_ID,
COPY_VERIFICATIONCODE_ID,
GENERATE_PASSWORD_ID,
NOOP_COMMAND_SUFFIX,
} from "./main-context-menu-handler";
export type CopyToClipboardOptions = { text: string; tab: chrome.tabs.Tab };
export type CopyToClipboardAction = (options: CopyToClipboardOptions) => void;
export type AutofillAction = (tab: chrome.tabs.Tab, cipher: CipherView) => Promise<void>;
export type GeneratePasswordToClipboardAction = (tab: chrome.tabs.Tab) => Promise<void>;
const NOT_IMPLEMENTED = (..._args: unknown[]) =>
Promise.reject<never>("This action is not implemented inside of a service worker context.");
export class ContextMenuClickedHandler {
constructor(
private copyToClipboard: CopyToClipboardAction,
private generatePasswordToClipboard: GeneratePasswordToClipboardAction,
private autofillAction: AutofillAction,
private authService: AuthService,
private cipherService: CipherService,
private totpService: TotpService,
private eventCollectionService: EventCollectionService
) {}
static async mv3Create(cachedServices: CachedServices) {
const stateFactory = new StateFactory(GlobalState, Account);
const serviceOptions: AuthServiceInitOptions & CipherServiceInitOptions = {
apiServiceOptions: {
logoutCallback: NOT_IMPLEMENTED,
},
cryptoFunctionServiceOptions: {
win: self,
},
encryptServiceOptions: {
logMacFailures: false,
},
i18nServiceOptions: {
systemLanguage: chrome.i18n.getUILanguage(),
},
keyConnectorServiceOptions: {
logoutCallback: NOT_IMPLEMENTED,
},
logServiceOptions: {
isDev: false,
},
platformUtilsServiceOptions: {
biometricCallback: NOT_IMPLEMENTED,
clipboardWriteCallback: NOT_IMPLEMENTED,
win: self,
},
stateMigrationServiceOptions: {
stateFactory: stateFactory,
},
stateServiceOptions: {
stateFactory: stateFactory,
},
};
const generatePasswordToClipboardCommand = new GeneratePasswordToClipboardCommand(
await passwordGenerationServiceFactory(cachedServices, serviceOptions),
await stateServiceFactory(cachedServices, serviceOptions)
);
const autofillCommand = new AutofillTabCommand(
await autofillServiceFactory(cachedServices, serviceOptions)
);
return new ContextMenuClickedHandler(
(options) => copyToClipboard(options.tab, options.text),
(tab) => generatePasswordToClipboardCommand.generatePasswordToClipboard(tab),
(tab, cipher) => autofillCommand.doAutofillTabWithCipherCommand(tab, cipher),
await authServiceFactory(cachedServices, serviceOptions),
await cipherServiceFactory(cachedServices, serviceOptions),
await totpServiceFactory(cachedServices, serviceOptions),
await eventCollectionServiceFactory(cachedServices, serviceOptions)
);
}
static async onClickedListener(
info: chrome.contextMenus.OnClickData,
tab?: chrome.tabs.Tab,
cachedServices: CachedServices = {}
) {
const contextMenuClickedHandler = await ContextMenuClickedHandler.mv3Create(cachedServices);
await contextMenuClickedHandler.run(info, tab);
}
static async messageListener(
message: { command: string; data: LockedVaultPendingNotificationsItem },
sender: chrome.runtime.MessageSender,
cachedServices: CachedServices
) {
if (
message.command !== "unlockCompleted" ||
message.data.target !== "contextmenus.background"
) {
return;
}
const contextMenuClickedHandler = await ContextMenuClickedHandler.mv3Create(cachedServices);
await contextMenuClickedHandler.run(
message.data.commandToRetry.msg.data,
message.data.commandToRetry.sender.tab
);
}
async run(info: chrome.contextMenus.OnClickData, tab?: chrome.tabs.Tab) {
switch (info.menuItemId) {
case GENERATE_PASSWORD_ID:
if (!tab) {
return;
}
await this.generatePasswordToClipboard(tab);
break;
case COPY_IDENTIFIER_ID:
if (!tab) {
return;
}
this.copyToClipboard({ text: await this.getIdentifier(tab, info), tab: tab });
break;
default:
await this.cipherAction(info, tab);
}
}
async cipherAction(info: chrome.contextMenus.OnClickData, tab?: chrome.tabs.Tab) {
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
const retryMessage: LockedVaultPendingNotificationsItem = {
commandToRetry: {
msg: { command: NOOP_COMMAND_SUFFIX, data: info },
sender: { tab: tab },
},
target: "contextmenus.background",
};
await BrowserApi.tabSendMessageData(
tab,
"addToLockedVaultPendingNotifications",
retryMessage
);
await BrowserApi.tabSendMessageData(tab, "promptForLogin");
return;
}
// NOTE: We don't actually use the first part of this ID, we further switch based on the parentMenuItemId
// I would really love to not add it but that is a departure from how it currently works.
const id = (info.menuItemId as string).split("_")[1]; // We create all the ids, we can guarantee they are strings
let cipher: CipherView | undefined;
if (id === NOOP_COMMAND_SUFFIX) {
// This NOOP item has come through which is generally only for no access state but since we got here
// we are actually unlocked we will do our best to find a good match of an item to autofill this is useful
// in scenarios like unlock on autofill
const ciphers = await this.cipherService.getAllDecryptedForUrl(tab.url);
cipher = ciphers.find((c) => c.reprompt === CipherRepromptType.None);
} else {
const ciphers = await this.cipherService.getAllDecrypted();
cipher = ciphers.find((c) => c.id === id);
}
if (cipher == null) {
return;
}
switch (info.parentMenuItemId) {
case AUTOFILL_ID:
if (tab == null) {
return;
}
await this.autofillAction(tab, cipher);
break;
case COPY_USERNAME_ID:
this.copyToClipboard({ text: cipher.login.username, tab: tab });
break;
case COPY_PASSWORD_ID:
this.copyToClipboard({ text: cipher.login.password, tab: tab });
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
break;
case COPY_VERIFICATIONCODE_ID:
this.copyToClipboard({ text: await this.totpService.getCode(cipher.login.totp), tab: tab });
break;
}
}
private async getIdentifier(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) {
return new Promise<string>((resolve, reject) => {
BrowserApi.sendTabsMessage(
tab.id,
{ command: "getClickedElement" },
{ frameId: info.frameId },
(identifier: string) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
}
resolve(identifier);
}
);
});
}
}