mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-24 02:41:54 +01:00
[PM-5189] Implementing the message-connector to ensure we are sending messages without posting a window message
This commit is contained in:
parent
3313807acb
commit
d6d52e5e5e
@ -12,10 +12,10 @@ type PageDetailsForTab = Record<
|
||||
>;
|
||||
|
||||
type SubFrameOffsetData = {
|
||||
frameId?: number;
|
||||
url?: string;
|
||||
top: number;
|
||||
left: number;
|
||||
frameId?: number;
|
||||
} | null;
|
||||
|
||||
type SubFrameOffsetsForTab = Record<
|
||||
@ -49,9 +49,9 @@ type OverlayBackgroundExtensionMessage = {
|
||||
forceCloseOverlay?: boolean;
|
||||
isOverlayHidden?: boolean;
|
||||
setTransparentOverlay?: boolean;
|
||||
data?: LockedVaultPendingNotificationsData;
|
||||
isFieldCurrentlyFocused?: boolean;
|
||||
isCurrentlyFilling?: boolean;
|
||||
data?: LockedVaultPendingNotificationsData;
|
||||
} & OverlayAddNewItemMessage;
|
||||
|
||||
type OverlayPortMessage = {
|
||||
@ -132,6 +132,7 @@ type OverlayButtonPortMessageHandlers = {
|
||||
forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void;
|
||||
overlayPageBlurred: () => void;
|
||||
redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||
getPageColorScheme: () => void;
|
||||
};
|
||||
|
||||
type OverlayListPortMessageHandlers = {
|
||||
@ -144,6 +145,7 @@ type OverlayListPortMessageHandlers = {
|
||||
addNewVaultItem: ({ port }: PortConnectionParam) => void;
|
||||
viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||
redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||
updateAutofillOverlayListHeight: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||
};
|
||||
|
||||
interface OverlayBackground {
|
||||
|
@ -95,6 +95,11 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
this.closeOverlay(port.sender, { forceCloseOverlay: true }),
|
||||
overlayPageBlurred: () => this.checkOverlayListFocused(),
|
||||
redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port),
|
||||
getPageColorScheme: () => {
|
||||
this.overlayButtonPort?.postMessage({
|
||||
command: "getPageColorScheme",
|
||||
});
|
||||
},
|
||||
};
|
||||
private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = {
|
||||
checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(),
|
||||
@ -106,6 +111,12 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port),
|
||||
viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port),
|
||||
redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port),
|
||||
updateAutofillOverlayListHeight: ({ message }) => {
|
||||
this.overlayListPort?.postMessage({
|
||||
command: "updateIframePosition",
|
||||
styles: message.styles,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
constructor(
|
||||
@ -832,7 +843,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
|
||||
}
|
||||
|
||||
private updateIsFieldCurrentlyFocused({ message }: OverlayBackgroundExtensionMessage) {
|
||||
private updateIsFieldCurrentlyFocused(message: OverlayBackgroundExtensionMessage) {
|
||||
this.isFieldCurrentlyFocused = message.isFieldCurrentlyFocused;
|
||||
}
|
||||
|
||||
@ -910,6 +921,14 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
* @param port - The port that connected to the extension background
|
||||
*/
|
||||
private handlePortOnConnect = async (port: chrome.runtime.Port) => {
|
||||
const isOverlayListMessageConnector = port.name === AutofillOverlayPort.ListMessageConnector;
|
||||
const isOverlayButtonMessageConnector =
|
||||
port.name === AutofillOverlayPort.ButtonMessageConnector;
|
||||
if (isOverlayListMessageConnector || isOverlayButtonMessageConnector) {
|
||||
port.onMessage.addListener(this.handleOverlayElementPortMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const isOverlayListPort = port.name === AutofillOverlayPort.List;
|
||||
const isOverlayButtonPort = port.name === AutofillOverlayPort.Button;
|
||||
|
||||
@ -923,7 +942,6 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
this.overlayButtonPort = port;
|
||||
}
|
||||
|
||||
port.onMessage.addListener(this.handleOverlayElementPortMessage);
|
||||
port.onDisconnect.addListener(this.handlePortOnDisconnect);
|
||||
port.postMessage({
|
||||
command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`,
|
||||
@ -932,6 +950,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
|
||||
translations: this.getTranslations(),
|
||||
ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null,
|
||||
messageConnectorUrl: chrome.runtime.getURL("overlay/message-connector.html"),
|
||||
});
|
||||
void this.updateOverlayPosition(
|
||||
{
|
||||
@ -956,11 +975,11 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
const command = message?.command;
|
||||
let handler: CallableFunction | undefined;
|
||||
|
||||
if (port.name === AutofillOverlayPort.Button) {
|
||||
if (port.name === AutofillOverlayPort.ButtonMessageConnector) {
|
||||
handler = this.overlayButtonPortMessageHandlers[command];
|
||||
}
|
||||
|
||||
if (port.name === AutofillOverlayPort.List) {
|
||||
if (port.name === AutofillOverlayPort.ListMessageConnector) {
|
||||
handler = this.overlayListPortMessageHandlers[command];
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers
|
||||
globalThis.addEventListener("load", load);
|
||||
function load() {
|
||||
setupWindowMessageListener();
|
||||
postMessageToParent({ command: "initNotificationBar" });
|
||||
postMessageToConnector({ command: "initNotificationBar" });
|
||||
}
|
||||
|
||||
function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
@ -392,6 +392,6 @@ function setNotificationBarTheme() {
|
||||
document.documentElement.classList.add(`theme_${theme}`);
|
||||
}
|
||||
|
||||
function postMessageToParent(message: NotificationBarWindowMessage) {
|
||||
function postMessageToConnector(message: NotificationBarWindowMessage) {
|
||||
globalThis.parent.postMessage(message, windowMessageOrigin || "*");
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ type UpdateAuthStatusMessage = OverlayButtonMessage & { authStatus: Authenticati
|
||||
type InitAutofillOverlayButtonMessage = UpdateAuthStatusMessage & {
|
||||
styleSheetUrl: string;
|
||||
translations: Record<string, string>;
|
||||
messageConnectorUrl: string;
|
||||
};
|
||||
|
||||
type OverlayButtonWindowMessageHandlers = {
|
||||
|
@ -7,7 +7,6 @@ type AutofillOverlayIframeExtensionMessage = {
|
||||
type AutofillOverlayIframeWindowMessageHandlers = {
|
||||
[key: string]: CallableFunction;
|
||||
updateAutofillOverlayListHeight: (message: AutofillOverlayIframeExtensionMessage) => void;
|
||||
getPageColorScheme: () => void;
|
||||
};
|
||||
|
||||
type AutofillOverlayIframeExtensionMessageParam = {
|
||||
@ -19,6 +18,7 @@ type BackgroundPortMessageHandlers = {
|
||||
initAutofillOverlayList: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void;
|
||||
updateIframePosition: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void;
|
||||
updateOverlayHidden: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void;
|
||||
getPageColorScheme: () => void;
|
||||
};
|
||||
|
||||
interface AutofillOverlayIframeService {
|
||||
|
@ -14,6 +14,7 @@ type InitAutofillOverlayListMessage = OverlayListMessage & {
|
||||
theme: string;
|
||||
translations: Record<string, string>;
|
||||
ciphers?: OverlayCipherData[];
|
||||
messageConnectorUrl: string;
|
||||
};
|
||||
|
||||
type OverlayListWindowMessageHandlers = {
|
||||
|
@ -70,13 +70,13 @@ export class InlineMenuElements implements InlineMenuElementsInterface {
|
||||
* unobserve the body element to ensure the mutation observer no
|
||||
* longer triggers.
|
||||
*/
|
||||
private removeInlineMenu = (message: any) => {
|
||||
if (message.overlayElement === AutofillOverlayElement.Button) {
|
||||
private removeInlineMenu = (message?: AutofillExtensionMessage) => {
|
||||
if (message?.overlayElement === AutofillOverlayElement.Button) {
|
||||
this.removeInlineMenuButton();
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.overlayElement === AutofillOverlayElement.List) {
|
||||
if (message?.overlayElement === AutofillOverlayElement.List) {
|
||||
this.removeInlineMenuList();
|
||||
return;
|
||||
}
|
||||
@ -408,7 +408,7 @@ export class InlineMenuElements implements InlineMenuElementsInterface {
|
||||
clearTimeout(this.mutationObserverIterationsResetTimeout);
|
||||
this.mutationObserverIterations = 0;
|
||||
void this.sendExtensionMessage("blurMostRecentOverlayField");
|
||||
this.removeInlineMenu({ forceClose: true });
|
||||
this.removeInlineMenu();
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -417,6 +417,6 @@ export class InlineMenuElements implements InlineMenuElementsInterface {
|
||||
}
|
||||
destroy() {
|
||||
this.documentElementMutationObserver?.disconnect();
|
||||
this.removeInlineMenu({ forceClose: true });
|
||||
this.removeInlineMenu();
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,15 @@
|
||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { setElementStyles } from "../../utils";
|
||||
import { sendExtensionMessage, setElementStyles } from "../../utils";
|
||||
import {
|
||||
BackgroundPortMessageHandlers,
|
||||
AutofillOverlayIframeService as AutofillOverlayIframeServiceInterface,
|
||||
AutofillOverlayIframeExtensionMessage,
|
||||
AutofillOverlayIframeWindowMessageHandlers,
|
||||
} from "../abstractions/autofill-overlay-iframe.service";
|
||||
|
||||
class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterface {
|
||||
private port: chrome.runtime.Port | null = null;
|
||||
private extensionOriginsSet: Set<string>;
|
||||
private iframeMutationObserver: MutationObserver;
|
||||
private iframe: HTMLIFrameElement;
|
||||
private ariaAlertElement: HTMLDivElement;
|
||||
@ -42,15 +40,11 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
|
||||
private foreignMutationsCount = 0;
|
||||
private mutationObserverIterations = 0;
|
||||
private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout;
|
||||
private readonly windowMessageHandlers: AutofillOverlayIframeWindowMessageHandlers = {
|
||||
updateAutofillOverlayListHeight: (message) =>
|
||||
this.updateElementStyles(this.iframe, message.styles),
|
||||
getPageColorScheme: () => this.updateOverlayPageColorScheme(),
|
||||
};
|
||||
private readonly backgroundPortMessageHandlers: BackgroundPortMessageHandlers = {
|
||||
initAutofillOverlayList: ({ message }) => this.initAutofillOverlayList(message),
|
||||
updateIframePosition: ({ message }) => this.updateIframePosition(message.styles),
|
||||
updateOverlayHidden: ({ message }) => this.updateElementStyles(this.iframe, message.styles),
|
||||
getPageColorScheme: () => this.updateOverlayPageColorScheme(),
|
||||
};
|
||||
|
||||
constructor(
|
||||
@ -58,11 +52,6 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
|
||||
private portName: string,
|
||||
private shadow: ShadowRoot,
|
||||
) {
|
||||
this.extensionOriginsSet = new Set([
|
||||
chrome.runtime.getURL("").slice(0, -1).toLowerCase(), // Remove the trailing slash and normalize the extension url to lowercase
|
||||
"null",
|
||||
]);
|
||||
|
||||
this.iframeMutationObserver = new MutationObserver(this.handleMutations);
|
||||
}
|
||||
|
||||
@ -134,7 +123,6 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
|
||||
this.port = chrome.runtime.connect({ name: this.portName });
|
||||
this.port.onDisconnect.addListener(this.handlePortDisconnect);
|
||||
this.port.onMessage.addListener(this.handlePortMessage);
|
||||
globalThis.addEventListener(EVENTS.MESSAGE, this.handleWindowMessage);
|
||||
|
||||
this.announceAriaAlert();
|
||||
};
|
||||
@ -168,7 +156,6 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
|
||||
}
|
||||
|
||||
this.updateElementStyles(this.iframe, { opacity: "0", height: "0px", display: "block" });
|
||||
globalThis.removeEventListener("message", this.handleWindowMessage);
|
||||
this.unobserveIframe();
|
||||
this.port?.onMessage.removeListener(this.handlePortMessage);
|
||||
this.port?.onDisconnect.removeListener(this.handlePortDisconnect);
|
||||
@ -265,30 +252,6 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles messages sent from the iframe. If the message does not have a
|
||||
* specified handler set, it passes the message to the background script.
|
||||
*
|
||||
* @param event - The message event
|
||||
*/
|
||||
private handleWindowMessage = (event: MessageEvent) => {
|
||||
if (
|
||||
!this.port ||
|
||||
event.source !== this.iframe.contentWindow ||
|
||||
!this.isFromExtensionOrigin(event.origin.toLowerCase())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = event.data;
|
||||
if (this.windowMessageHandlers[message.command]) {
|
||||
this.windowMessageHandlers[message.command](message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.port.postMessage(event.data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Accepts an element and updates the styles for that element. This method
|
||||
* will also unobserve the element if it is the iframe element. This is
|
||||
@ -311,17 +274,6 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
|
||||
this.observeIframe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Chrome returns null for any sandboxed iframe sources.
|
||||
* Firefox references the extension URI as its origin.
|
||||
* Any other origin value is a security risk.
|
||||
*
|
||||
* @param messageOrigin - The origin of the window message
|
||||
*/
|
||||
private isFromExtensionOrigin(messageOrigin: string): boolean {
|
||||
return this.extensionOriginsSet.has(messageOrigin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mutations to the iframe element. The ensures that the iframe
|
||||
* element's styles are not modified by a third party source.
|
||||
@ -351,6 +303,10 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
|
||||
}
|
||||
};
|
||||
|
||||
private forceCloseAutofillOverlay() {
|
||||
void sendExtensionMessage("closeAutofillOverlay", { forceClose: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mutations to the iframe element's attributes. This ensures that
|
||||
* the iframe element's attributes are not modified by a third party source.
|
||||
@ -366,7 +322,7 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
|
||||
}
|
||||
|
||||
if (this.foreignMutationsCount >= 10) {
|
||||
this.port?.postMessage({ command: "forceCloseAutofillOverlay" });
|
||||
this.forceCloseAutofillOverlay();
|
||||
break;
|
||||
}
|
||||
|
||||
@ -421,7 +377,7 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
|
||||
if (this.mutationObserverIterations > 20) {
|
||||
clearTimeout(this.mutationObserverIterationsResetTimeout);
|
||||
resetCounters();
|
||||
this.port?.postMessage({ command: "forceCloseAutofillOverlay" });
|
||||
this.forceCloseAutofillOverlay();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -46,15 +46,20 @@ class AutofillOverlayButton extends AutofillOverlayPageElement {
|
||||
* @param authStatus - The authentication status of the user
|
||||
* @param styleSheetUrl - The URL of the stylesheet to apply to the page
|
||||
* @param translations - The translations to apply to the page
|
||||
* @private
|
||||
* @param messageConnectorUrl - The URL of the message connector to use
|
||||
*/
|
||||
private async initAutofillOverlayButton({
|
||||
authStatus,
|
||||
styleSheetUrl,
|
||||
translations,
|
||||
messageConnectorUrl,
|
||||
}: InitAutofillOverlayButtonMessage) {
|
||||
const linkElement = this.initOverlayPage("button", styleSheetUrl, translations);
|
||||
|
||||
const linkElement = await this.initOverlayPage(
|
||||
"button",
|
||||
styleSheetUrl,
|
||||
translations,
|
||||
messageConnectorUrl,
|
||||
);
|
||||
this.buttonElement.tabIndex = -1;
|
||||
this.buttonElement.type = "button";
|
||||
this.buttonElement.classList.add("overlay-button");
|
||||
@ -63,7 +68,7 @@ class AutofillOverlayButton extends AutofillOverlayPageElement {
|
||||
this.getTranslation("toggleBitwardenVaultOverlay"),
|
||||
);
|
||||
this.buttonElement.addEventListener(EVENTS.CLICK, this.handleButtonElementClick);
|
||||
this.postMessageToParent({ command: "getPageColorScheme" });
|
||||
this.postMessageToConnector({ command: "getPageColorScheme" });
|
||||
|
||||
this.updateAuthStatus(authStatus);
|
||||
|
||||
@ -103,7 +108,7 @@ class AutofillOverlayButton extends AutofillOverlayPageElement {
|
||||
* parent window indicating that the button was clicked.
|
||||
*/
|
||||
private handleButtonElementClick = () => {
|
||||
this.postMessageToParent({ command: "overlayButtonClicked" });
|
||||
this.postMessageToConnector({ command: "overlayButtonClicked" });
|
||||
};
|
||||
|
||||
/**
|
||||
@ -115,7 +120,7 @@ class AutofillOverlayButton extends AutofillOverlayPageElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.postMessageToParent({ command: "closeAutofillOverlay" });
|
||||
this.postMessageToConnector({ command: "closeAutofillOverlay" });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
|
||||
* @param theme - The theme to use for the overlay list.
|
||||
* @param authStatus - The current authentication status.
|
||||
* @param ciphers - The ciphers to display in the overlay list.
|
||||
* @param messageConnectorUrl - The URL of the message connector to use.
|
||||
*/
|
||||
private async initAutofillOverlayList({
|
||||
translations,
|
||||
@ -51,8 +52,14 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
|
||||
theme,
|
||||
authStatus,
|
||||
ciphers,
|
||||
messageConnectorUrl,
|
||||
}: InitAutofillOverlayListMessage) {
|
||||
const linkElement = this.initOverlayPage("button", styleSheetUrl, translations);
|
||||
const linkElement = await this.initOverlayPage(
|
||||
"list",
|
||||
styleSheetUrl,
|
||||
translations,
|
||||
messageConnectorUrl,
|
||||
);
|
||||
|
||||
const themeClass = `theme_${theme}`;
|
||||
globalThis.document.documentElement.classList.add(themeClass);
|
||||
@ -105,7 +112,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
|
||||
* Sends a message to the parent window to unlock the vault.
|
||||
*/
|
||||
private handleUnlockButtonClick = () => {
|
||||
this.postMessageToParent({ command: "unlockVault" });
|
||||
this.postMessageToConnector({ command: "unlockVault" });
|
||||
};
|
||||
|
||||
/**
|
||||
@ -169,7 +176,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
|
||||
* Sends a message to the parent window to add a new vault item.
|
||||
*/
|
||||
private handeNewItemButtonClick = () => {
|
||||
this.postMessageToParent({ command: "addNewVaultItem" });
|
||||
this.postMessageToConnector({ command: "addNewVaultItem" });
|
||||
};
|
||||
|
||||
/**
|
||||
@ -276,7 +283,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
|
||||
private handleFillCipherClickEvent = (cipher: OverlayCipherData) => {
|
||||
return this.useEventHandlersMemo(
|
||||
() =>
|
||||
this.postMessageToParent({
|
||||
this.postMessageToConnector({
|
||||
command: "fillSelectedListItem",
|
||||
overlayCipherId: cipher.id,
|
||||
}),
|
||||
@ -341,7 +348,8 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
|
||||
*/
|
||||
private handleViewCipherClickEvent = (cipher: OverlayCipherData) => {
|
||||
return this.useEventHandlersMemo(
|
||||
() => this.postMessageToParent({ command: "viewSelectedCipher", overlayCipherId: cipher.id }),
|
||||
() =>
|
||||
this.postMessageToConnector({ command: "viewSelectedCipher", overlayCipherId: cipher.id }),
|
||||
`${cipher.id}-view-cipher-button-click-handler`,
|
||||
);
|
||||
};
|
||||
@ -476,7 +484,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.postMessageToParent({ command: "checkAutofillOverlayButtonFocused" });
|
||||
this.postMessageToConnector({ command: "checkAutofillOverlayButtonFocused" });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -533,7 +541,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
|
||||
}
|
||||
|
||||
const { height } = entry.contentRect;
|
||||
this.postMessageToParent({
|
||||
this.postMessageToConnector({
|
||||
command: "updateAutofillOverlayListHeight",
|
||||
styles: { height: `${height}px` },
|
||||
});
|
||||
|
@ -0,0 +1,3 @@
|
||||
import { AutofillOverlayMessageConnector } from "./message-connector";
|
||||
|
||||
(() => new AutofillOverlayMessageConnector())();
|
@ -0,0 +1,10 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Autofill overlay message connector</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="color-scheme" content="normal" />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
@ -0,0 +1,46 @@
|
||||
export class AutofillOverlayMessageConnector {
|
||||
private extensionOriginsSet: Set<string>;
|
||||
private port: chrome.runtime.Port | null = null;
|
||||
|
||||
constructor() {
|
||||
globalThis.addEventListener("message", this.handleWindowMessage);
|
||||
|
||||
this.extensionOriginsSet = new Set([
|
||||
chrome.runtime.getURL("").slice(0, -1).toLowerCase(), // Remove the trailing slash and normalize the extension url to lowercase
|
||||
"null",
|
||||
]);
|
||||
}
|
||||
|
||||
private handleWindowMessage = (event: MessageEvent) => {
|
||||
if (
|
||||
event.source !== globalThis.parent ||
|
||||
!this.isFromExtensionOrigin(event.origin.toLowerCase())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = event.data;
|
||||
|
||||
if (this.port) {
|
||||
this.port.postMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.command !== "initAutofillOverlayPort") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.port = chrome.runtime.connect({ name: message.portName });
|
||||
};
|
||||
|
||||
/**
|
||||
* Chrome returns null for any sandboxed iframe sources.
|
||||
* Firefox references the extension URI as its origin.
|
||||
* Any other origin value is a security risk.
|
||||
*
|
||||
* @param messageOrigin - The origin of the window message
|
||||
*/
|
||||
private isFromExtensionOrigin(messageOrigin: string): boolean {
|
||||
return this.extensionOriginsSet.has(messageOrigin);
|
||||
}
|
||||
}
|
@ -31,11 +31,12 @@ describe("AutofillOverlayPageElement", () => {
|
||||
jest.spyOn(globalThis.document, "createElement");
|
||||
});
|
||||
|
||||
it("initializes the button overlay page", () => {
|
||||
const linkElement = autofillOverlayPageElement["initOverlayPage"](
|
||||
it("initializes the button overlay page", async () => {
|
||||
const linkElement = await autofillOverlayPageElement["initOverlayPage"](
|
||||
"button",
|
||||
"https://jest-testing-website.com",
|
||||
translations,
|
||||
"https://jest-testing-website.com/message-connector",
|
||||
);
|
||||
|
||||
expect(globalThis.document.documentElement.setAttribute).toHaveBeenCalledWith(
|
||||
@ -49,16 +50,16 @@ describe("AutofillOverlayPageElement", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("postMessageToParent", () => {
|
||||
describe("postMessageToConnector", () => {
|
||||
it("skips posting a message to the parent if the message origin in not set", () => {
|
||||
autofillOverlayPageElement["postMessageToParent"]({ command: "test" });
|
||||
autofillOverlayPageElement["postMessageToConnector"]({ command: "test" });
|
||||
|
||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("posts a message to the parent", () => {
|
||||
autofillOverlayPageElement["messageOrigin"] = "https://jest-testing-website.com";
|
||||
autofillOverlayPageElement["postMessageToParent"]({ command: "test" });
|
||||
autofillOverlayPageElement["postMessageToConnector"]({ command: "test" });
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "test" },
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
|
||||
import { RedirectFocusDirection } from "../../../utils/autofill-overlay.enum";
|
||||
import { AutofillOverlayPort, RedirectFocusDirection } from "../../../utils/autofill-overlay.enum";
|
||||
import {
|
||||
AutofillOverlayPageElementWindowMessage,
|
||||
WindowMessageHandlers,
|
||||
@ -10,6 +10,7 @@ class AutofillOverlayPageElement extends HTMLElement {
|
||||
protected shadowDom: ShadowRoot;
|
||||
protected messageOrigin: string;
|
||||
protected translations: Record<string, string>;
|
||||
protected messageConnectorIframe: HTMLIFrameElement;
|
||||
protected windowMessageHandlers: WindowMessageHandlers;
|
||||
|
||||
constructor() {
|
||||
@ -25,16 +26,41 @@ class AutofillOverlayPageElement extends HTMLElement {
|
||||
* @param elementName - The name of the element, e.g. "button" or "list"
|
||||
* @param styleSheetUrl - The URL of the stylesheet to apply to the page
|
||||
* @param translations - The translations to apply to the page
|
||||
* @param messageConnectorUrl - The URL of the message connector to use
|
||||
*/
|
||||
protected initOverlayPage(
|
||||
protected async initOverlayPage(
|
||||
elementName: "button" | "list",
|
||||
styleSheetUrl: string,
|
||||
translations: Record<string, string>,
|
||||
): HTMLLinkElement {
|
||||
messageConnectorUrl: string,
|
||||
): Promise<HTMLLinkElement> {
|
||||
this.translations = translations;
|
||||
globalThis.document.documentElement.setAttribute("lang", this.getTranslation("locale"));
|
||||
globalThis.document.head.title = this.getTranslation(`${elementName}PageTitle`);
|
||||
|
||||
this.messageConnectorIframe = globalThis.document.createElement("iframe");
|
||||
this.messageConnectorIframe.src = messageConnectorUrl;
|
||||
this.messageConnectorIframe.style.opacity = "0";
|
||||
this.messageConnectorIframe.style.position = "absolute";
|
||||
this.messageConnectorIframe.style.width = "0";
|
||||
this.messageConnectorIframe.style.height = "0";
|
||||
this.messageConnectorIframe.style.border = "none";
|
||||
this.messageConnectorIframe.style.pointerEvents = "none";
|
||||
globalThis.document.body.appendChild(this.messageConnectorIframe);
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
this.messageConnectorIframe.addEventListener(EVENTS.LOAD, () => {
|
||||
this.postMessageToConnector({
|
||||
command: `initAutofillOverlayPort`,
|
||||
portName:
|
||||
elementName === "list"
|
||||
? AutofillOverlayPort.ListMessageConnector
|
||||
: AutofillOverlayPort.ButtonMessageConnector,
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
this.shadowDom.innerHTML = "";
|
||||
const linkElement = globalThis.document.createElement("link");
|
||||
linkElement.setAttribute("rel", "stylesheet");
|
||||
@ -48,12 +74,12 @@ class AutofillOverlayPageElement extends HTMLElement {
|
||||
*
|
||||
* @param message - The message to post
|
||||
*/
|
||||
protected postMessageToParent(message: AutofillOverlayPageElementWindowMessage) {
|
||||
protected postMessageToConnector(message: AutofillOverlayPageElementWindowMessage) {
|
||||
if (!this.messageOrigin) {
|
||||
return;
|
||||
}
|
||||
|
||||
globalThis.parent.postMessage(message, this.messageOrigin);
|
||||
this.messageConnectorIframe.contentWindow.postMessage(message, "*");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -111,7 +137,7 @@ class AutofillOverlayPageElement extends HTMLElement {
|
||||
* Handles the window blur event.
|
||||
*/
|
||||
private handleWindowBlurEvent = () => {
|
||||
this.postMessageToParent({ command: "overlayPageBlurred" });
|
||||
this.postMessageToConnector({ command: "overlayPageBlurred" });
|
||||
};
|
||||
|
||||
/**
|
||||
@ -148,7 +174,7 @@ class AutofillOverlayPageElement extends HTMLElement {
|
||||
* @param direction - The direction to redirect the focus out
|
||||
*/
|
||||
private redirectOverlayFocusOutMessage(direction: string) {
|
||||
this.postMessageToParent({ command: "redirectOverlayFocusOut", direction });
|
||||
this.postMessageToConnector({ command: "redirectOverlayFocusOut", direction });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,6 +173,7 @@ function createInitAutofillOverlayButtonMessageMock(
|
||||
translations: overlayPagesTranslations,
|
||||
styleSheetUrl: "https://jest-testing-website.com",
|
||||
authStatus: AuthenticationStatus.Unlocked,
|
||||
messageConnectorUrl: "https://jest-testing-website.com/message-connector",
|
||||
...customFields,
|
||||
};
|
||||
}
|
||||
@ -203,6 +204,7 @@ function createInitAutofillOverlayListMessageMock(
|
||||
styleSheetUrl: "https://jest-testing-website.com",
|
||||
theme: ThemeType.Light,
|
||||
authStatus: AuthenticationStatus.Unlocked,
|
||||
messageConnectorUrl: "https://jest-testing-website.com/message-connector",
|
||||
ciphers: [
|
||||
createAutofillOverlayCipherDataMock(1, {
|
||||
icon: {
|
||||
|
@ -5,7 +5,9 @@ const AutofillOverlayElement = {
|
||||
|
||||
const AutofillOverlayPort = {
|
||||
Button: "autofill-overlay-button-port",
|
||||
ButtonMessageConnector: "autofill-overlay-button-message-connector",
|
||||
List: "autofill-overlay-list-port",
|
||||
ListMessageConnector: "autofill-overlay-list-message-connector",
|
||||
} as const;
|
||||
|
||||
const RedirectFocusDirection = {
|
||||
|
@ -115,6 +115,7 @@
|
||||
"images/icon38_locked.png",
|
||||
"overlay/button.html",
|
||||
"overlay/list.html",
|
||||
"overlay/message-connector.html",
|
||||
"popup/fonts/*"
|
||||
],
|
||||
"applications": {
|
||||
|
@ -119,6 +119,7 @@
|
||||
"images/icon38_locked.png",
|
||||
"overlay/button.html",
|
||||
"overlay/list.html",
|
||||
"overlay/message-connector.html",
|
||||
"popup/fonts/*"
|
||||
],
|
||||
"matches": ["<all_urls>"]
|
||||
|
@ -116,6 +116,11 @@ const plugins = [
|
||||
filename: "overlay/list.html",
|
||||
chunks: ["overlay/list"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/autofill/overlay/pages/message-connector/message-connector.html",
|
||||
filename: "overlay/message-connector.html",
|
||||
chunks: ["overlay/message-connector"],
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
manifestVersion == 3
|
||||
@ -173,6 +178,8 @@ const mainConfig = {
|
||||
"notification/bar": "./src/autofill/notification/bar.ts",
|
||||
"overlay/button": "./src/autofill/overlay/pages/button/bootstrap-autofill-overlay-button.ts",
|
||||
"overlay/list": "./src/autofill/overlay/pages/list/bootstrap-autofill-overlay-list.ts",
|
||||
"overlay/message-connector":
|
||||
"./src/autofill/overlay/pages/message-connector/bootstrap-autofill-overlay-message-connector.ts",
|
||||
"encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.ts",
|
||||
"content/lp-fileless-importer": "./src/tools/content/lp-fileless-importer.ts",
|
||||
"content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts",
|
||||
|
Loading…
Reference in New Issue
Block a user