diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index b0bd5f2246..27b7ff1163 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -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 { diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index e96a5106ab..3f54340f10 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -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]; } diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index a730ee1eba..61ea034fb0 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -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 || "*"); } diff --git a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-button.ts b/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-button.ts index b6b22be943..3fa34693d3 100644 --- a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-button.ts +++ b/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-button.ts @@ -7,6 +7,7 @@ type UpdateAuthStatusMessage = OverlayButtonMessage & { authStatus: Authenticati type InitAutofillOverlayButtonMessage = UpdateAuthStatusMessage & { styleSheetUrl: string; translations: Record; + messageConnectorUrl: string; }; type OverlayButtonWindowMessageHandlers = { diff --git a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-iframe.service.ts b/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-iframe.service.ts index 0c4160a070..2357ca2b8f 100644 --- a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-iframe.service.ts +++ b/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-iframe.service.ts @@ -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 { diff --git a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-list.ts b/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-list.ts index b656f238dc..ce49d5c85c 100644 --- a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-list.ts +++ b/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-list.ts @@ -14,6 +14,7 @@ type InitAutofillOverlayListMessage = OverlayListMessage & { theme: string; translations: Record; ciphers?: OverlayCipherData[]; + messageConnectorUrl: string; }; type OverlayListWindowMessageHandlers = { diff --git a/apps/browser/src/autofill/overlay/content/inline-menu-elements.ts b/apps/browser/src/autofill/overlay/content/inline-menu-elements.ts index 9b0cbf4e9a..64298802f4 100644 --- a/apps/browser/src/autofill/overlay/content/inline-menu-elements.ts +++ b/apps/browser/src/autofill/overlay/content/inline-menu-elements.ts @@ -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(); } } diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe.service.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe.service.ts index b7a6f2a39e..7bfa676d53 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe.service.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe.service.ts @@ -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; 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; } diff --git a/apps/browser/src/autofill/overlay/pages/button/autofill-overlay-button.ts b/apps/browser/src/autofill/overlay/pages/button/autofill-overlay-button.ts index 0d68b37e2f..57ea6f89d7 100644 --- a/apps/browser/src/autofill/overlay/pages/button/autofill-overlay-button.ts +++ b/apps/browser/src/autofill/overlay/pages/button/autofill-overlay-button.ts @@ -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" }); } } diff --git a/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.ts b/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.ts index 4892ec4abf..4a5466ba3e 100644 --- a/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.ts +++ b/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.ts @@ -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` }, }); diff --git a/apps/browser/src/autofill/overlay/pages/message-connector/bootstrap-autofill-overlay-message-connector.ts b/apps/browser/src/autofill/overlay/pages/message-connector/bootstrap-autofill-overlay-message-connector.ts new file mode 100644 index 0000000000..ef1ba474b9 --- /dev/null +++ b/apps/browser/src/autofill/overlay/pages/message-connector/bootstrap-autofill-overlay-message-connector.ts @@ -0,0 +1,3 @@ +import { AutofillOverlayMessageConnector } from "./message-connector"; + +(() => new AutofillOverlayMessageConnector())(); diff --git a/apps/browser/src/autofill/overlay/pages/message-connector/message-connector.html b/apps/browser/src/autofill/overlay/pages/message-connector/message-connector.html new file mode 100644 index 0000000000..77ea7c9ae3 --- /dev/null +++ b/apps/browser/src/autofill/overlay/pages/message-connector/message-connector.html @@ -0,0 +1,10 @@ + + + + Autofill overlay message connector + + + + + + diff --git a/apps/browser/src/autofill/overlay/pages/message-connector/message-connector.ts b/apps/browser/src/autofill/overlay/pages/message-connector/message-connector.ts new file mode 100644 index 0000000000..9f6340cd2f --- /dev/null +++ b/apps/browser/src/autofill/overlay/pages/message-connector/message-connector.ts @@ -0,0 +1,46 @@ +export class AutofillOverlayMessageConnector { + private extensionOriginsSet: Set; + 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); + } +} diff --git a/apps/browser/src/autofill/overlay/pages/shared/autofill-overlay-page-element.spec.ts b/apps/browser/src/autofill/overlay/pages/shared/autofill-overlay-page-element.spec.ts index c70892cfca..ce369b6b2d 100644 --- a/apps/browser/src/autofill/overlay/pages/shared/autofill-overlay-page-element.spec.ts +++ b/apps/browser/src/autofill/overlay/pages/shared/autofill-overlay-page-element.spec.ts @@ -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" }, diff --git a/apps/browser/src/autofill/overlay/pages/shared/autofill-overlay-page-element.ts b/apps/browser/src/autofill/overlay/pages/shared/autofill-overlay-page-element.ts index 34f115f401..9dffd7801d 100644 --- a/apps/browser/src/autofill/overlay/pages/shared/autofill-overlay-page-element.ts +++ b/apps/browser/src/autofill/overlay/pages/shared/autofill-overlay-page-element.ts @@ -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; + 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, - ): HTMLLinkElement { + messageConnectorUrl: string, + ): Promise { 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((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 }); } } diff --git a/apps/browser/src/autofill/spec/autofill-mocks.ts b/apps/browser/src/autofill/spec/autofill-mocks.ts index 708489c57e..3b87591ccc 100644 --- a/apps/browser/src/autofill/spec/autofill-mocks.ts +++ b/apps/browser/src/autofill/spec/autofill-mocks.ts @@ -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: { diff --git a/apps/browser/src/autofill/utils/autofill-overlay.enum.ts b/apps/browser/src/autofill/utils/autofill-overlay.enum.ts index 486d68f754..ed2030324d 100644 --- a/apps/browser/src/autofill/utils/autofill-overlay.enum.ts +++ b/apps/browser/src/autofill/utils/autofill-overlay.enum.ts @@ -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 = { diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index d1979b4e23..89421309b7 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -115,6 +115,7 @@ "images/icon38_locked.png", "overlay/button.html", "overlay/list.html", + "overlay/message-connector.html", "popup/fonts/*" ], "applications": { diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index e7b0c0cd1e..23a0e06c4d 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -119,6 +119,7 @@ "images/icon38_locked.png", "overlay/button.html", "overlay/list.html", + "overlay/message-connector.html", "popup/fonts/*" ], "matches": [""] diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 3b5724b198..82d12086a4 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -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",