diff --git a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts index ab3b429dbfa..a147e0ba165 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts @@ -2,7 +2,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { InlineMenuCipherData } from "../../../background/abstractions/overlay.background"; -type AutofillInlineMenuContainerMessage = { +export type AutofillInlineMenuContainerMessage = { command: string; portKey: string; }; @@ -18,8 +18,14 @@ export type InitAutofillInlineMenuElementMessage = AutofillInlineMenuContainerMe portName: string; }; +export type AutofillInlineMenuContainerWindowMessage = AutofillInlineMenuContainerMessage & + Record; + +export type AutofillInlineMenuContainerPortMessage = AutofillInlineMenuContainerMessage & + Record; + export type AutofillInlineMenuContainerWindowMessageHandlers = { [key: string]: CallableFunction; - initAutofillInlineMenuList: (message: InitAutofillInlineMenuElementMessage) => void; initAutofillInlineMenuButton: (message: InitAutofillInlineMenuElementMessage) => void; + initAutofillInlineMenuList: (message: InitAutofillInlineMenuElementMessage) => void; }; diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts new file mode 100644 index 00000000000..f7a5727e47f --- /dev/null +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts @@ -0,0 +1,130 @@ +import { AutofillOverlayPort } from "../../../../enums/autofill-overlay.enum"; +import { createPortSpyMock } from "../../../../spec/autofill-mocks"; +import { postWindowMessage } from "../../../../spec/testing-utils"; + +import { AutofillInlineMenuContainer } from "./autofill-inline-menu-container"; + +describe("AutofillInlineMenuContainer", () => { + const portKey = "testPortKey"; + const iframeUrl = "https://example.com"; + const pageTitle = "Example"; + let autofillInlineMenuContainer: AutofillInlineMenuContainer; + + beforeEach(() => { + autofillInlineMenuContainer = new AutofillInlineMenuContainer(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("initializing the inline menu iframe", () => { + it("sets the default iframe attributes to the message values", () => { + const message = { + command: "initAutofillInlineMenuList", + iframeUrl, + pageTitle, + portKey, + portName: AutofillOverlayPort.List, + }; + + postWindowMessage(message); + + expect(autofillInlineMenuContainer["defaultIframeAttributes"].src).toBe(message.iframeUrl); + expect(autofillInlineMenuContainer["defaultIframeAttributes"].title).toBe(message.pageTitle); + expect(autofillInlineMenuContainer["portName"]).toBe(message.portName); + }); + + it("sets up a onLoad listener on the iframe that sets up the background port message listener", async () => { + const message = { + command: "initAutofillInlineMenuButton", + iframeUrl, + pageTitle, + portKey, + portName: AutofillOverlayPort.Button, + }; + + postWindowMessage(message); + + jest.spyOn(autofillInlineMenuContainer["inlineMenuPageIframe"].contentWindow, "postMessage"); + autofillInlineMenuContainer["inlineMenuPageIframe"].dispatchEvent(new Event("load")); + + expect(chrome.runtime.connect).toHaveBeenCalledWith({ name: message.portName }); + expect( + autofillInlineMenuContainer["inlineMenuPageIframe"].contentWindow.postMessage, + ).toHaveBeenCalledWith(message, "*"); + }); + }); + + describe("handling window messages", () => { + let iframe: HTMLIFrameElement; + let port: chrome.runtime.Port; + + beforeEach(() => { + const message = { + command: "initAutofillInlineMenuButton", + iframeUrl, + pageTitle, + portKey, + portName: AutofillOverlayPort.Button, + }; + + postWindowMessage(message); + + iframe = autofillInlineMenuContainer["inlineMenuPageIframe"]; + jest.spyOn(iframe.contentWindow, "postMessage"); + port = createPortSpyMock(AutofillOverlayPort.Button); + autofillInlineMenuContainer["port"] = port; + }); + + it("ignores messages that do not contain a portKey", () => { + const message = { command: "checkInlineMenuButtonFocused" }; + + postWindowMessage(message, "*", iframe.contentWindow as any); + + expect(port.postMessage).not.toHaveBeenCalled(); + }); + + it("ignores messages if the inline menu iframe has not been created", () => { + autofillInlineMenuContainer["inlineMenuPageIframe"] = null; + const message = { command: "checkInlineMenuButtonFocused", portKey }; + + postWindowMessage(message, "*", iframe.contentWindow as any); + + expect(port.postMessage).not.toHaveBeenCalled(); + }); + + it("ignores messages that do not come from either the parent frame or the inline menu iframe", () => { + const randomIframe = document.createElement("iframe"); + const message = { command: "checkInlineMenuButtonFocused", portKey }; + + postWindowMessage(message, "*", randomIframe.contentWindow as any); + + expect(port.postMessage).not.toHaveBeenCalled(); + }); + + it("ignores messages that come from an invalid origin", () => { + const message = { command: "checkInlineMenuButtonFocused", portKey }; + + postWindowMessage(message, "https://example.com", iframe.contentWindow as any); + + expect(port.postMessage).not.toHaveBeenCalled(); + }); + + it("posts a message to the background from the inline menu iframe", () => { + const message = { command: "checkInlineMenuButtonFocused", portKey }; + + postWindowMessage(message, "null", iframe.contentWindow as any); + + expect(port.postMessage).toHaveBeenCalledWith(message); + }); + + it("posts a message to the inline menu iframe from the parent", () => { + const message = { command: "checkInlineMenuButtonFocused", portKey }; + + postWindowMessage(message); + + expect(iframe.contentWindow.postMessage).toHaveBeenCalledWith(message, "*"); + }); + }); +}); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts index 24bb23fd549..54d1472a478 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts @@ -4,9 +4,12 @@ import { setElementStyles } from "../../../../utils"; import { InitAutofillInlineMenuElementMessage, AutofillInlineMenuContainerWindowMessageHandlers, + AutofillInlineMenuContainerWindowMessage, + AutofillInlineMenuContainerPortMessage, } from "../../abstractions/autofill-inline-menu-container"; export class AutofillInlineMenuContainer { + private setElementStyles = setElementStyles; private extensionOriginsSet: Set; private port: chrome.runtime.Port | null = null; private portName: string; @@ -37,8 +40,8 @@ export class AutofillInlineMenuContainer { tabIndex: "-1", }; private windowMessageHandlers: AutofillInlineMenuContainerWindowMessageHandlers = { - initAutofillInlineMenuList: (message) => this.handleInitInlineMenuIframe(message), initAutofillInlineMenuButton: (message) => this.handleInitInlineMenuIframe(message), + initAutofillInlineMenuList: (message) => this.handleInitInlineMenuIframe(message), }; constructor() { @@ -50,44 +53,67 @@ export class AutofillInlineMenuContainer { globalThis.addEventListener("message", this.handleWindowMessage); } + /** + * Handles initialization of the iframe used to display the inline menu. + * + * @param message - The message containing the iframe url and page title. + */ private handleInitInlineMenuIframe(message: InitAutofillInlineMenuElementMessage) { this.defaultIframeAttributes.src = message.iframeUrl; this.defaultIframeAttributes.title = message.pageTitle; this.portName = message.portName; this.inlineMenuPageIframe = globalThis.document.createElement("iframe"); - setElementStyles(this.inlineMenuPageIframe, this.iframeStyles, true); + this.setElementStyles(this.inlineMenuPageIframe, this.iframeStyles, true); for (const [attribute, value] of Object.entries(this.defaultIframeAttributes)) { this.inlineMenuPageIframe.setAttribute(attribute, value); } - this.inlineMenuPageIframe.addEventListener(EVENTS.LOAD, () => - this.setupPortMessageListener(message), - ); + const handleInlineMenuPageIframeLoad = () => { + this.inlineMenuPageIframe.removeEventListener(EVENTS.LOAD, handleInlineMenuPageIframeLoad); + this.setupPortMessageListener(message); + }; + this.inlineMenuPageIframe.addEventListener(EVENTS.LOAD, handleInlineMenuPageIframeLoad); globalThis.document.body.appendChild(this.inlineMenuPageIframe); } + /** + * Sets up the port message listener for the inline menu page. + * + * @param message - The message containing the port name. + */ private setupPortMessageListener = (message: InitAutofillInlineMenuElementMessage) => { this.port = chrome.runtime.connect({ name: this.portName }); this.postMessageToInlineMenuPage(message); }; - private postMessageToInlineMenuPage(message: any) { - if (!this.inlineMenuPageIframe?.contentWindow) { - return; + /** + * Posts a message to the inline menu page iframe. + * + * @param message - The message to post. + */ + private postMessageToInlineMenuPage(message: AutofillInlineMenuContainerWindowMessage) { + if (this.inlineMenuPageIframe?.contentWindow) { + this.inlineMenuPageIframe.contentWindow.postMessage(message, "*"); } - - this.inlineMenuPageIframe.contentWindow.postMessage(message, "*"); } - private postMessageToBackground(message: any) { - if (!this.port) { - return; + /** + * Posts a message from the inline menu iframe to the background script. + * + * @param message - The message to post. + */ + private postMessageToBackground(message: AutofillInlineMenuContainerPortMessage) { + if (this.port) { + this.port.postMessage(message); } - - this.port.postMessage(message); } + /** + * Handles window messages, routing them to the appropriate handler. + * + * @param event - The message event. + */ private handleWindowMessage = (event: MessageEvent) => { const message = event.data; if (this.isForeignWindowMessage(event)) { @@ -107,6 +133,13 @@ export class AutofillInlineMenuContainer { this.postMessageToBackground(message); }; + /** + * Identifies if the message is from a foreign window. A foreign window message is + * considered as any message that does not have a portKey, is not from the parent window, + * or is not from the inline menu page iframe. + * + * @param event - The message event. + */ private isForeignWindowMessage(event: MessageEvent) { if (!event.data.portKey) { return true; @@ -119,10 +152,20 @@ export class AutofillInlineMenuContainer { return !this.isMessageFromInlineMenuPageIframe(event); } + /** + * Identifies if the message is from the parent window. + * + * @param event - The message event. + */ private isMessageFromParentWindow(event: MessageEvent): boolean { return globalThis.parent === event.source; } + /** + * Identifies if the message is from the inline menu page iframe. + * + * @param event - The message event. + */ private isMessageFromInlineMenuPageIframe(event: MessageEvent): boolean { if (!this.inlineMenuPageIframe) { return false;