bitwarden-estensione-browser/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts

190 lines
6.0 KiB
TypeScript

import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { Utils } from "@bitwarden/common/platform/misc/utils";
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 { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
authServiceFactory,
AuthServiceInitOptions,
} from "../../auth/background/service-factories/auth-service.factory";
import { Account } from "../../models/account";
import { CachedServices } from "../../platform/background/service-factories/factory-options";
import { BrowserApi } from "../../platform/browser/browser-api";
import {
cipherServiceFactory,
CipherServiceInitOptions,
} from "../../vault/background/service_factories/cipher-service.factory";
import { MainContextMenuHandler } from "./main-context-menu-handler";
const NOT_IMPLEMENTED = (..._args: unknown[]) => Promise.resolve();
const LISTENED_TO_COMMANDS = [
"loggedIn",
"unlocked",
"syncCompleted",
"bgUpdateContextMenu",
"editedCipher",
"addedCipher",
"deletedCipher",
];
export class CipherContextMenuHandler {
constructor(
private mainContextMenuHandler: MainContextMenuHandler,
private authService: AuthService,
private cipherService: CipherService
) {}
static async create(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: () => Promise.resolve(false),
clipboardWriteCallback: NOT_IMPLEMENTED,
win: self,
},
stateMigrationServiceOptions: {
stateFactory: stateFactory,
},
stateServiceOptions: {
stateFactory: stateFactory,
},
};
return new CipherContextMenuHandler(
await MainContextMenuHandler.mv3Create(cachedServices),
await authServiceFactory(cachedServices, serviceOptions),
await cipherServiceFactory(cachedServices, serviceOptions)
);
}
static async tabsOnActivatedListener(
activeInfo: chrome.tabs.TabActiveInfo,
serviceCache: CachedServices
) {
const cipherContextMenuHandler = await CipherContextMenuHandler.create(serviceCache);
const tab = await BrowserApi.getTab(activeInfo.tabId);
await cipherContextMenuHandler.update(tab.url);
}
static async tabsOnReplacedListener(
addedTabId: number,
removedTabId: number,
serviceCache: CachedServices
) {
const cipherContextMenuHandler = await CipherContextMenuHandler.create(serviceCache);
const tab = await BrowserApi.getTab(addedTabId);
await cipherContextMenuHandler.update(tab.url);
}
static async tabsOnUpdatedListener(
tabId: number,
changeInfo: chrome.tabs.TabChangeInfo,
tab: chrome.tabs.Tab,
serviceCache: CachedServices
) {
if (changeInfo.status !== "complete") {
return;
}
const cipherContextMenuHandler = await CipherContextMenuHandler.create(serviceCache);
await cipherContextMenuHandler.update(tab.url);
}
static async messageListener(
message: { command: string },
sender: chrome.runtime.MessageSender,
cachedServices: CachedServices
) {
if (!CipherContextMenuHandler.shouldListen(message)) {
return;
}
const cipherContextMenuHandler = await CipherContextMenuHandler.create(cachedServices);
await cipherContextMenuHandler.messageListener(message);
}
private static shouldListen(message: { command: string }) {
return LISTENED_TO_COMMANDS.includes(message.command);
}
async messageListener(message: { command: string }, sender?: chrome.runtime.MessageSender) {
if (!CipherContextMenuHandler.shouldListen(message)) {
return;
}
const activeTabs = await BrowserApi.getActiveTabs();
if (!activeTabs || activeTabs.length === 0) {
return;
}
await this.update(activeTabs[0].url);
}
async update(url: string) {
const authStatus = await this.authService.getAuthStatus();
await MainContextMenuHandler.removeAll();
if (authStatus !== AuthenticationStatus.Unlocked) {
// Should I pass in the auth status or even have two separate methods for this
// on MainContextMenuHandler
await this.mainContextMenuHandler.noAccess();
return;
}
const menuEnabled = await this.mainContextMenuHandler.init();
if (!menuEnabled) {
return;
}
const ciphers = await this.cipherService.getAllDecryptedForUrl(url);
ciphers.sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
if (ciphers.length === 0) {
await this.mainContextMenuHandler.noLogins(url);
return;
}
for (const cipher of ciphers) {
await this.updateForCipher(url, cipher);
}
}
private async updateForCipher(url: string, cipher: CipherView) {
if (
cipher == null ||
cipher.type !== CipherType.Login ||
cipher.reprompt !== CipherRepromptType.None
) {
return;
}
let title = cipher.name;
if (!Utils.isNullOrEmpty(title)) {
title += ` (${cipher.login.username})`;
}
await this.mainContextMenuHandler.loadOptions(title, cipher.id, url, cipher);
}
}