mirror of
https://github.com/bitwarden/browser.git
synced 2024-06-25 10:25:36 +02:00
Refactor environment service to emit a single observable. This required significant changes to how the environment service behaves and tackles much of the tech debt planned for it.
741 lines
27 KiB
TypeScript
741 lines
27 KiB
TypeScript
import { firstValueFrom } from "rxjs";
|
|
|
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
|
import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants";
|
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
|
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
|
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
|
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
|
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
|
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
|
|
|
import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
|
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
|
import {
|
|
openViewVaultItemPopout,
|
|
openAddEditVaultItemPopout,
|
|
} from "../../vault/popup/utils/vault-popout-window";
|
|
import { AutofillService, PageDetail } from "../services/abstractions/autofill.service";
|
|
import { AutofillOverlayElement, AutofillOverlayPort } from "../utils/autofill-overlay.enum";
|
|
|
|
import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background";
|
|
import {
|
|
FocusedFieldData,
|
|
OverlayBackgroundExtensionMessageHandlers,
|
|
OverlayButtonPortMessageHandlers,
|
|
OverlayCipherData,
|
|
OverlayListPortMessageHandlers,
|
|
OverlayBackground as OverlayBackgroundInterface,
|
|
OverlayBackgroundExtensionMessage,
|
|
OverlayAddNewItemMessage,
|
|
OverlayPortMessage,
|
|
WebsiteIconData,
|
|
} from "./abstractions/overlay.background";
|
|
|
|
class OverlayBackground implements OverlayBackgroundInterface {
|
|
private readonly openUnlockPopout = openUnlockPopout;
|
|
private readonly openViewVaultItemPopout = openViewVaultItemPopout;
|
|
private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout;
|
|
private overlayLoginCiphers: Map<string, CipherView> = new Map();
|
|
private pageDetailsForTab: Record<number, PageDetail[]> = {};
|
|
private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut;
|
|
private overlayButtonPort: chrome.runtime.Port;
|
|
private overlayListPort: chrome.runtime.Port;
|
|
private focusedFieldData: FocusedFieldData;
|
|
private overlayPageTranslations: Record<string, string>;
|
|
private iconsServerUrl: string;
|
|
private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = {
|
|
openAutofillOverlay: () => this.openOverlay(false),
|
|
autofillOverlayElementClosed: ({ message }) => this.overlayElementClosed(message),
|
|
autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender),
|
|
getAutofillOverlayVisibility: () => this.getOverlayVisibility(),
|
|
checkAutofillOverlayFocused: () => this.checkOverlayFocused(),
|
|
focusAutofillOverlayList: () => this.focusOverlayList(),
|
|
updateAutofillOverlayPosition: ({ message }) => this.updateOverlayPosition(message),
|
|
updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message),
|
|
updateFocusedFieldData: ({ message }) => this.setFocusedFieldData(message),
|
|
collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
|
|
unlockCompleted: ({ message }) => this.unlockCompleted(message),
|
|
addEditCipherSubmitted: () => this.updateOverlayCiphers(),
|
|
deletedCipher: () => this.updateOverlayCiphers(),
|
|
};
|
|
private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = {
|
|
overlayButtonClicked: ({ port }) => this.handleOverlayButtonClicked(port),
|
|
closeAutofillOverlay: ({ port }) => this.closeOverlay(port),
|
|
forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true),
|
|
overlayPageBlurred: () => this.checkOverlayListFocused(),
|
|
redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port),
|
|
};
|
|
private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = {
|
|
checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(),
|
|
forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true),
|
|
overlayPageBlurred: () => this.checkOverlayButtonFocused(),
|
|
unlockVault: ({ port }) => this.unlockVault(port),
|
|
fillSelectedListItem: ({ message, port }) => this.fillSelectedOverlayListItem(message, port),
|
|
addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port),
|
|
viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port),
|
|
redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port),
|
|
};
|
|
|
|
constructor(
|
|
private cipherService: CipherService,
|
|
private autofillService: AutofillService,
|
|
private authService: AuthService,
|
|
private environmentService: EnvironmentService,
|
|
private domainSettingsService: DomainSettingsService,
|
|
private stateService: StateService,
|
|
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
|
private i18nService: I18nService,
|
|
private platformUtilsService: PlatformUtilsService,
|
|
private themeStateService: ThemeStateService,
|
|
) {}
|
|
|
|
/**
|
|
* Removes cached page details for a tab
|
|
* based on the passed tabId.
|
|
*
|
|
* @param tabId - Used to reference the page details of a specific tab
|
|
*/
|
|
removePageDetails(tabId: number) {
|
|
delete this.pageDetailsForTab[tabId];
|
|
}
|
|
|
|
/**
|
|
* Sets up the extension message listeners and gets the settings for the
|
|
* overlay's visibility and the user's authentication status.
|
|
*/
|
|
async init() {
|
|
this.setupExtensionMessageListeners();
|
|
const env = await firstValueFrom(this.environmentService.environment$);
|
|
this.iconsServerUrl = env.getIconsUrl();
|
|
await this.getOverlayVisibility();
|
|
await this.getAuthStatus();
|
|
}
|
|
|
|
/**
|
|
* Updates the overlay list's ciphers and sends the updated list to the overlay list iframe.
|
|
* Queries all ciphers for the given url, and sorts them by last used. Will not update the
|
|
* list of ciphers if the extension is not unlocked.
|
|
*/
|
|
async updateOverlayCiphers() {
|
|
if (this.userAuthStatus !== AuthenticationStatus.Unlocked) {
|
|
return;
|
|
}
|
|
|
|
const currentTab = await BrowserApi.getTabFromCurrentWindowId();
|
|
if (!currentTab?.url) {
|
|
return;
|
|
}
|
|
|
|
this.overlayLoginCiphers = new Map();
|
|
const ciphersViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url)).sort(
|
|
(a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b),
|
|
);
|
|
for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) {
|
|
this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]);
|
|
}
|
|
|
|
const ciphers = await this.getOverlayCipherData();
|
|
this.overlayListPort?.postMessage({ command: "updateOverlayListCiphers", ciphers });
|
|
await BrowserApi.tabSendMessageData(currentTab, "updateIsOverlayCiphersPopulated", {
|
|
isOverlayCiphersPopulated: Boolean(ciphers.length),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Strips out unnecessary data from the ciphers and returns an array of
|
|
* objects that contain the cipher data needed for the overlay list.
|
|
*/
|
|
private async getOverlayCipherData(): Promise<OverlayCipherData[]> {
|
|
const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$);
|
|
const overlayCiphersArray = Array.from(this.overlayLoginCiphers);
|
|
const overlayCipherData = [];
|
|
let loginCipherIcon: WebsiteIconData;
|
|
|
|
for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) {
|
|
const [overlayCipherId, cipher] = overlayCiphersArray[cipherIndex];
|
|
if (!loginCipherIcon && cipher.type === CipherType.Login) {
|
|
loginCipherIcon = buildCipherIcon(this.iconsServerUrl, cipher, showFavicons);
|
|
}
|
|
|
|
overlayCipherData.push({
|
|
id: overlayCipherId,
|
|
name: cipher.name,
|
|
type: cipher.type,
|
|
reprompt: cipher.reprompt,
|
|
favorite: cipher.favorite,
|
|
icon:
|
|
cipher.type === CipherType.Login
|
|
? loginCipherIcon
|
|
: buildCipherIcon(this.iconsServerUrl, cipher, showFavicons),
|
|
login: cipher.type === CipherType.Login ? { username: cipher.login.username } : null,
|
|
card: cipher.type === CipherType.Card ? cipher.card.subTitle : null,
|
|
});
|
|
}
|
|
|
|
return overlayCipherData;
|
|
}
|
|
|
|
/**
|
|
* Handles aggregation of page details for a tab. Stores the page details
|
|
* in association with the tabId of the tab that sent the message.
|
|
*
|
|
* @param message - Message received from the `collectPageDetailsResponse` command
|
|
* @param sender - The sender of the message
|
|
*/
|
|
private storePageDetails(
|
|
message: OverlayBackgroundExtensionMessage,
|
|
sender: chrome.runtime.MessageSender,
|
|
) {
|
|
const pageDetails = {
|
|
frameId: sender.frameId,
|
|
tab: sender.tab,
|
|
details: message.details,
|
|
};
|
|
|
|
if (this.pageDetailsForTab[sender.tab.id]?.length) {
|
|
this.pageDetailsForTab[sender.tab.id].push(pageDetails);
|
|
return;
|
|
}
|
|
|
|
this.pageDetailsForTab[sender.tab.id] = [pageDetails];
|
|
}
|
|
|
|
/**
|
|
* Triggers autofill for the selected cipher in the overlay list. Also places
|
|
* the selected cipher at the top of the list of ciphers.
|
|
*
|
|
* @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID.
|
|
* @param sender - The sender of the port message
|
|
*/
|
|
private async fillSelectedOverlayListItem(
|
|
{ overlayCipherId }: OverlayPortMessage,
|
|
{ sender }: chrome.runtime.Port,
|
|
) {
|
|
if (!overlayCipherId) {
|
|
return;
|
|
}
|
|
|
|
const cipher = this.overlayLoginCiphers.get(overlayCipherId);
|
|
|
|
if (await this.autofillService.isPasswordRepromptRequired(cipher, sender.tab)) {
|
|
return;
|
|
}
|
|
const totpCode = await this.autofillService.doAutoFill({
|
|
tab: sender.tab,
|
|
cipher: cipher,
|
|
pageDetails: this.pageDetailsForTab[sender.tab.id],
|
|
fillNewPassword: true,
|
|
allowTotpAutofill: true,
|
|
});
|
|
|
|
if (totpCode) {
|
|
this.platformUtilsService.copyToClipboard(totpCode);
|
|
}
|
|
|
|
this.overlayLoginCiphers = new Map([[overlayCipherId, cipher], ...this.overlayLoginCiphers]);
|
|
}
|
|
|
|
/**
|
|
* Checks if the overlay is focused. Will check the overlay list
|
|
* if it is open, otherwise it will check the overlay button.
|
|
*/
|
|
private checkOverlayFocused() {
|
|
if (this.overlayListPort) {
|
|
this.checkOverlayListFocused();
|
|
|
|
return;
|
|
}
|
|
|
|
this.checkOverlayButtonFocused();
|
|
}
|
|
|
|
/**
|
|
* Posts a message to the overlay button iframe to check if it is focused.
|
|
*/
|
|
private checkOverlayButtonFocused() {
|
|
this.overlayButtonPort?.postMessage({ command: "checkAutofillOverlayButtonFocused" });
|
|
}
|
|
|
|
/**
|
|
* Posts a message to the overlay list iframe to check if it is focused.
|
|
*/
|
|
private checkOverlayListFocused() {
|
|
this.overlayListPort?.postMessage({ command: "checkAutofillOverlayListFocused" });
|
|
}
|
|
|
|
/**
|
|
* Sends a message to the sender tab to close the autofill overlay.
|
|
*
|
|
* @param sender - The sender of the port message
|
|
* @param forceCloseOverlay - Identifies whether the overlay should be force closed
|
|
*/
|
|
private closeOverlay({ sender }: chrome.runtime.Port, forceCloseOverlay = false) {
|
|
// 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
|
|
BrowserApi.tabSendMessageData(sender.tab, "closeAutofillOverlay", { forceCloseOverlay });
|
|
}
|
|
|
|
/**
|
|
* Handles cleanup when an overlay element is closed. Disconnects
|
|
* the list and button ports and sets them to null.
|
|
*
|
|
* @param overlayElement - The overlay element that was closed, either the list or button
|
|
*/
|
|
private overlayElementClosed({ overlayElement }: OverlayBackgroundExtensionMessage) {
|
|
if (overlayElement === AutofillOverlayElement.Button) {
|
|
this.overlayButtonPort?.disconnect();
|
|
this.overlayButtonPort = null;
|
|
|
|
return;
|
|
}
|
|
|
|
this.overlayListPort?.disconnect();
|
|
this.overlayListPort = null;
|
|
}
|
|
|
|
/**
|
|
* Updates the position of either the overlay list or button. The position
|
|
* is based on the focused field's position and dimensions.
|
|
*
|
|
* @param overlayElement - The overlay element to update, either the list or button
|
|
*/
|
|
private updateOverlayPosition({ overlayElement }: { overlayElement?: string }) {
|
|
if (!overlayElement) {
|
|
return;
|
|
}
|
|
|
|
if (overlayElement === AutofillOverlayElement.Button) {
|
|
this.overlayButtonPort?.postMessage({
|
|
command: "updateIframePosition",
|
|
styles: this.getOverlayButtonPosition(),
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
this.overlayListPort?.postMessage({
|
|
command: "updateIframePosition",
|
|
styles: this.getOverlayListPosition(),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gets the position of the focused field and calculates the position
|
|
* of the overlay button based on the focused field's position and dimensions.
|
|
*/
|
|
private getOverlayButtonPosition() {
|
|
if (!this.focusedFieldData) {
|
|
return;
|
|
}
|
|
|
|
const { top, left, width, height } = this.focusedFieldData.focusedFieldRects;
|
|
const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles;
|
|
let elementOffset = height * 0.37;
|
|
if (height >= 35) {
|
|
elementOffset = height >= 50 ? height * 0.47 : height * 0.42;
|
|
}
|
|
|
|
const elementHeight = height - elementOffset;
|
|
const elementTopPosition = top + elementOffset / 2;
|
|
let elementLeftPosition = left + width - height + elementOffset / 2;
|
|
|
|
const fieldPaddingRight = parseInt(paddingRight, 10);
|
|
const fieldPaddingLeft = parseInt(paddingLeft, 10);
|
|
if (fieldPaddingRight > fieldPaddingLeft) {
|
|
elementLeftPosition = left + width - height - (fieldPaddingRight - elementOffset + 2);
|
|
}
|
|
|
|
return {
|
|
top: `${Math.round(elementTopPosition)}px`,
|
|
left: `${Math.round(elementLeftPosition)}px`,
|
|
height: `${Math.round(elementHeight)}px`,
|
|
width: `${Math.round(elementHeight)}px`,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Gets the position of the focused field and calculates the position
|
|
* of the overlay list based on the focused field's position and dimensions.
|
|
*/
|
|
private getOverlayListPosition() {
|
|
if (!this.focusedFieldData) {
|
|
return;
|
|
}
|
|
|
|
const { top, left, width, height } = this.focusedFieldData.focusedFieldRects;
|
|
return {
|
|
width: `${Math.round(width)}px`,
|
|
top: `${Math.round(top + height)}px`,
|
|
left: `${Math.round(left)}px`,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Sets the focused field data to the data passed in the extension message.
|
|
*
|
|
* @param focusedFieldData - Contains the rects and styles of the focused field.
|
|
*/
|
|
private setFocusedFieldData({ focusedFieldData }: OverlayBackgroundExtensionMessage) {
|
|
this.focusedFieldData = focusedFieldData;
|
|
}
|
|
|
|
/**
|
|
* Updates the overlay's visibility based on the display property passed in the extension message.
|
|
*
|
|
* @param display - The display property of the overlay, either "block" or "none"
|
|
*/
|
|
private updateOverlayHidden({ display }: OverlayBackgroundExtensionMessage) {
|
|
if (!display) {
|
|
return;
|
|
}
|
|
|
|
const portMessage = { command: "updateOverlayHidden", styles: { display } };
|
|
|
|
this.overlayButtonPort?.postMessage(portMessage);
|
|
this.overlayListPort?.postMessage(portMessage);
|
|
}
|
|
|
|
/**
|
|
* Sends a message to the currently active tab to open the autofill overlay.
|
|
*
|
|
* @param isFocusingFieldElement - Identifies whether the field element should be focused when the overlay is opened
|
|
* @param isOpeningFullOverlay - Identifies whether the full overlay should be forced open regardless of other states
|
|
*/
|
|
private async openOverlay(isFocusingFieldElement = false, isOpeningFullOverlay = false) {
|
|
const currentTab = await BrowserApi.getTabFromCurrentWindowId();
|
|
|
|
await BrowserApi.tabSendMessageData(currentTab, "openAutofillOverlay", {
|
|
isFocusingFieldElement,
|
|
isOpeningFullOverlay,
|
|
authStatus: await this.getAuthStatus(),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gets the overlay's visibility setting from the settings service.
|
|
*/
|
|
private async getOverlayVisibility(): Promise<InlineMenuVisibilitySetting> {
|
|
return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$);
|
|
}
|
|
|
|
/**
|
|
* Gets the user's authentication status from the auth service. If the user's
|
|
* authentication status has changed, the overlay button's authentication status
|
|
* will be updated and the overlay list's ciphers will be updated.
|
|
*/
|
|
private async getAuthStatus() {
|
|
const formerAuthStatus = this.userAuthStatus;
|
|
this.userAuthStatus = await this.authService.getAuthStatus();
|
|
|
|
if (
|
|
this.userAuthStatus !== formerAuthStatus &&
|
|
this.userAuthStatus === AuthenticationStatus.Unlocked
|
|
) {
|
|
this.updateOverlayButtonAuthStatus();
|
|
await this.updateOverlayCiphers();
|
|
}
|
|
|
|
return this.userAuthStatus;
|
|
}
|
|
|
|
/**
|
|
* Sends a message to the overlay button to update its authentication status.
|
|
*/
|
|
private updateOverlayButtonAuthStatus() {
|
|
this.overlayButtonPort?.postMessage({
|
|
command: "updateOverlayButtonAuthStatus",
|
|
authStatus: this.userAuthStatus,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handles the overlay button being clicked. If the user is not authenticated,
|
|
* the vault will be unlocked. If the user is authenticated, the overlay will
|
|
* be opened.
|
|
*
|
|
* @param port - The port of the overlay button
|
|
*/
|
|
private handleOverlayButtonClicked(port: chrome.runtime.Port) {
|
|
if (this.userAuthStatus !== AuthenticationStatus.Unlocked) {
|
|
// 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
|
|
this.unlockVault(port);
|
|
return;
|
|
}
|
|
|
|
// 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
|
|
this.openOverlay(false, true);
|
|
}
|
|
|
|
/**
|
|
* Facilitates opening the unlock popout window.
|
|
*
|
|
* @param port - The port of the overlay list
|
|
*/
|
|
private async unlockVault(port: chrome.runtime.Port) {
|
|
const { sender } = port;
|
|
|
|
this.closeOverlay(port);
|
|
const retryMessage: LockedVaultPendingNotificationsData = {
|
|
commandToRetry: { message: { command: "openAutofillOverlay" }, sender },
|
|
target: "overlay.background",
|
|
};
|
|
await BrowserApi.tabSendMessageData(
|
|
sender.tab,
|
|
"addToLockedVaultPendingNotifications",
|
|
retryMessage,
|
|
);
|
|
await this.openUnlockPopout(sender.tab, true);
|
|
}
|
|
|
|
/**
|
|
* Triggers the opening of a vault item popout window associated
|
|
* with the passed cipher ID.
|
|
* @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID.
|
|
* @param sender - The sender of the port message
|
|
*/
|
|
private async viewSelectedCipher(
|
|
{ overlayCipherId }: OverlayPortMessage,
|
|
{ sender }: chrome.runtime.Port,
|
|
) {
|
|
const cipher = this.overlayLoginCiphers.get(overlayCipherId);
|
|
if (!cipher) {
|
|
return;
|
|
}
|
|
|
|
await this.openViewVaultItemPopout(sender.tab, {
|
|
cipherId: cipher.id,
|
|
action: SHOW_AUTOFILL_BUTTON,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Facilitates redirecting focus to the overlay list.
|
|
*/
|
|
private focusOverlayList() {
|
|
this.overlayListPort?.postMessage({ command: "focusOverlayList" });
|
|
}
|
|
|
|
/**
|
|
* Updates the authentication status for the user and opens the overlay if
|
|
* a followup command is present in the message.
|
|
*
|
|
* @param message - Extension message received from the `unlockCompleted` command
|
|
*/
|
|
private async unlockCompleted(message: OverlayBackgroundExtensionMessage) {
|
|
await this.getAuthStatus();
|
|
|
|
if (message.data?.commandToRetry?.message?.command === "openAutofillOverlay") {
|
|
await this.openOverlay(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the translations for the overlay page.
|
|
*/
|
|
private getTranslations() {
|
|
if (!this.overlayPageTranslations) {
|
|
this.overlayPageTranslations = {
|
|
locale: BrowserApi.getUILanguage(),
|
|
opensInANewWindow: this.i18nService.translate("opensInANewWindow"),
|
|
buttonPageTitle: this.i18nService.translate("bitwardenOverlayButton"),
|
|
toggleBitwardenVaultOverlay: this.i18nService.translate("toggleBitwardenVaultOverlay"),
|
|
listPageTitle: this.i18nService.translate("bitwardenVault"),
|
|
unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewMatchingLogins"),
|
|
unlockAccount: this.i18nService.translate("unlockAccount"),
|
|
fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"),
|
|
partialUsername: this.i18nService.translate("partialUsername"),
|
|
view: this.i18nService.translate("view"),
|
|
noItemsToShow: this.i18nService.translate("noItemsToShow"),
|
|
newItem: this.i18nService.translate("newItem"),
|
|
addNewVaultItem: this.i18nService.translate("addNewVaultItem"),
|
|
};
|
|
}
|
|
|
|
return this.overlayPageTranslations;
|
|
}
|
|
|
|
/**
|
|
* Facilitates redirecting focus out of one of the
|
|
* overlay elements to elements on the page.
|
|
*
|
|
* @param direction - The direction to redirect focus to (either "next", "previous" or "current)
|
|
* @param sender - The sender of the port message
|
|
*/
|
|
private redirectOverlayFocusOut(
|
|
{ direction }: OverlayPortMessage,
|
|
{ sender }: chrome.runtime.Port,
|
|
) {
|
|
if (!direction) {
|
|
return;
|
|
}
|
|
|
|
// 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
|
|
BrowserApi.tabSendMessageData(sender.tab, "redirectOverlayFocusOut", { direction });
|
|
}
|
|
|
|
/**
|
|
* Triggers adding a new vault item from the overlay. Gathers data
|
|
* input by the user before calling to open the add/edit window.
|
|
*
|
|
* @param sender - The sender of the port message
|
|
*/
|
|
private getNewVaultItemDetails({ sender }: chrome.runtime.Port) {
|
|
// 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
|
|
BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" });
|
|
}
|
|
|
|
/**
|
|
* Handles adding a new vault item from the overlay. Gathers data login
|
|
* data captured in the extension message.
|
|
*
|
|
* @param login - The login data captured from the extension message
|
|
* @param sender - The sender of the extension message
|
|
*/
|
|
private async addNewVaultItem(
|
|
{ login }: OverlayAddNewItemMessage,
|
|
sender: chrome.runtime.MessageSender,
|
|
) {
|
|
if (!login) {
|
|
return;
|
|
}
|
|
|
|
const uriView = new LoginUriView();
|
|
uriView.uri = login.uri;
|
|
|
|
const loginView = new LoginView();
|
|
loginView.uris = [uriView];
|
|
loginView.username = login.username || "";
|
|
loginView.password = login.password || "";
|
|
|
|
const cipherView = new CipherView();
|
|
cipherView.name = (Utils.getHostname(login.uri) || login.hostname).replace(/^www\./, "");
|
|
cipherView.folderId = null;
|
|
cipherView.type = CipherType.Login;
|
|
cipherView.login = loginView;
|
|
|
|
await this.stateService.setAddEditCipherInfo({
|
|
cipher: cipherView,
|
|
collectionIds: cipherView.collectionIds,
|
|
});
|
|
|
|
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
|
|
await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
|
|
}
|
|
|
|
/**
|
|
* Sets up the extension message listeners for the overlay.
|
|
*/
|
|
private setupExtensionMessageListeners() {
|
|
BrowserApi.messageListener("overlay.background", this.handleExtensionMessage);
|
|
BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect);
|
|
}
|
|
|
|
/**
|
|
* Handles extension messages sent to the extension background.
|
|
*
|
|
* @param message - The message received from the extension
|
|
* @param sender - The sender of the message
|
|
* @param sendResponse - The response to send back to the sender
|
|
*/
|
|
private handleExtensionMessage = (
|
|
message: OverlayBackgroundExtensionMessage,
|
|
sender: chrome.runtime.MessageSender,
|
|
sendResponse: (response?: any) => void,
|
|
) => {
|
|
const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command];
|
|
if (!handler) {
|
|
return;
|
|
}
|
|
|
|
const messageResponse = handler({ message, sender });
|
|
if (!messageResponse) {
|
|
return;
|
|
}
|
|
|
|
// 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
|
|
Promise.resolve(messageResponse).then((response) => sendResponse(response));
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Handles the connection of a port to the extension background.
|
|
*
|
|
* @param port - The port that connected to the extension background
|
|
*/
|
|
private handlePortOnConnect = async (port: chrome.runtime.Port) => {
|
|
const isOverlayListPort = port.name === AutofillOverlayPort.List;
|
|
const isOverlayButtonPort = port.name === AutofillOverlayPort.Button;
|
|
|
|
if (!isOverlayListPort && !isOverlayButtonPort) {
|
|
return;
|
|
}
|
|
|
|
if (isOverlayListPort) {
|
|
this.overlayListPort = port;
|
|
} else {
|
|
this.overlayButtonPort = port;
|
|
}
|
|
|
|
port.onMessage.addListener(this.handleOverlayElementPortMessage);
|
|
port.postMessage({
|
|
command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`,
|
|
authStatus: await this.getAuthStatus(),
|
|
styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`),
|
|
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
|
|
translations: this.getTranslations(),
|
|
ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null,
|
|
});
|
|
this.updateOverlayPosition({
|
|
overlayElement: isOverlayListPort
|
|
? AutofillOverlayElement.List
|
|
: AutofillOverlayElement.Button,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Handles messages sent to the overlay list or button ports.
|
|
*
|
|
* @param message - The message received from the port
|
|
* @param port - The port that sent the message
|
|
*/
|
|
private handleOverlayElementPortMessage = (
|
|
message: OverlayBackgroundExtensionMessage,
|
|
port: chrome.runtime.Port,
|
|
) => {
|
|
const command = message?.command;
|
|
let handler: CallableFunction | undefined;
|
|
|
|
if (port.name === AutofillOverlayPort.Button) {
|
|
handler = this.overlayButtonPortMessageHandlers[command];
|
|
}
|
|
|
|
if (port.name === AutofillOverlayPort.List) {
|
|
handler = this.overlayListPortMessageHandlers[command];
|
|
}
|
|
|
|
if (!handler) {
|
|
return;
|
|
}
|
|
|
|
handler({ message, port });
|
|
};
|
|
}
|
|
|
|
export default OverlayBackground;
|