1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-19 02:51:14 +02:00

[PM-5189] Working through content script port improvement

This commit is contained in:
Cesar Gonzalez 2024-06-19 01:55:03 -05:00
parent 6802cc8957
commit da357f46b3
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
11 changed files with 88 additions and 29 deletions

View File

@ -73,13 +73,6 @@ export type OverlayBackgroundExtensionMessage = {
CloseInlineMenuMessage & CloseInlineMenuMessage &
ToggleInlineMenuHiddenMessage; ToggleInlineMenuHiddenMessage;
export type OverlayPortMessage = {
[key: string]: any;
command: string;
direction?: string;
inlineMenuCipherId?: string;
};
export type InlineMenuCipherData = { export type InlineMenuCipherData = {
id: string; id: string;
name: string; name: string;
@ -101,7 +94,7 @@ export type BackgroundOnMessageHandlerParams = BackgroundMessageParam & Backgrou
export type OverlayBackgroundExtensionMessageHandlers = { export type OverlayBackgroundExtensionMessageHandlers = {
[key: string]: CallableFunction; [key: string]: CallableFunction;
autofillOverlayElementClosed: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
triggerAutofillOverlayReposition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; triggerAutofillOverlayReposition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
checkIsInlineMenuCiphersPopulated: ({ sender }: BackgroundSenderParam) => void; checkIsInlineMenuCiphersPopulated: ({ sender }: BackgroundSenderParam) => void;
@ -137,6 +130,11 @@ export type OverlayBackgroundExtensionMessageHandlers = {
deletedCipher: () => void; deletedCipher: () => void;
}; };
export type OverlayPortMessage = OverlayBackgroundExtensionMessage & {
direction?: string;
inlineMenuCipherId?: string;
};
export type PortMessageParam = { export type PortMessageParam = {
message: OverlayPortMessage; message: OverlayPortMessage;
}; };
@ -145,6 +143,11 @@ export type PortConnectionParam = {
}; };
export type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam; export type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam;
export type OverlayContentScriptPortMessageHandlers = {
[key: string]: CallableFunction;
autofillOverlayElementClosed: ({ message, port }: PortOnMessageHandlerParams) => void;
};
export type InlineMenuButtonPortMessageHandlers = { export type InlineMenuButtonPortMessageHandlers = {
[key: string]: CallableFunction; [key: string]: CallableFunction;
triggerDelayedAutofillInlineMenuClosure: ({ port }: PortConnectionParam) => void; triggerDelayedAutofillInlineMenuClosure: ({ port }: PortConnectionParam) => void;

View File

@ -50,6 +50,7 @@ import {
SubFrameOffsetsForTab, SubFrameOffsetsForTab,
CloseInlineMenuMessage, CloseInlineMenuMessage,
ToggleInlineMenuHiddenMessage, ToggleInlineMenuHiddenMessage,
OverlayContentScriptPortMessageHandlers,
} from "./abstractions/overlay.background"; } from "./abstractions/overlay.background";
export class OverlayBackground implements OverlayBackgroundInterface { export class OverlayBackground implements OverlayBackgroundInterface {
@ -75,8 +76,6 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private isFieldCurrentlyFilling: boolean = false; private isFieldCurrentlyFilling: boolean = false;
private iconsServerUrl: string; private iconsServerUrl: string;
private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = {
autofillOverlayElementClosed: ({ message, sender }) =>
this.overlayElementClosed(message, sender),
autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender), autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender),
triggerAutofillOverlayReposition: ({ sender }) => this.triggerOverlayReposition(sender), triggerAutofillOverlayReposition: ({ sender }) => this.triggerOverlayReposition(sender),
checkIsInlineMenuCiphersPopulated: ({ sender }) => checkIsInlineMenuCiphersPopulated: ({ sender }) =>
@ -110,6 +109,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
editedCipher: () => this.updateInlineMenuCiphers(), editedCipher: () => this.updateInlineMenuCiphers(),
deletedCipher: () => this.updateInlineMenuCiphers(), deletedCipher: () => this.updateInlineMenuCiphers(),
}; };
private readonly contentScriptPortMessageHandlers: OverlayContentScriptPortMessageHandlers = {
autofillOverlayElementClosed: ({ message, port }) => this.overlayElementClosed(message, port),
};
private readonly inlineMenuButtonPortMessageHandlers: InlineMenuButtonPortMessageHandlers = { private readonly inlineMenuButtonPortMessageHandlers: InlineMenuButtonPortMessageHandlers = {
triggerDelayedAutofillInlineMenuClosure: ({ port }) => this.triggerDelayedInlineMenuClosure(), triggerDelayedAutofillInlineMenuClosure: ({ port }) => this.triggerDelayedInlineMenuClosure(),
autofillInlineMenuButtonClicked: ({ port }) => this.handleInlineMenuButtonClicked(port), autofillInlineMenuButtonClicked: ({ port }) => this.handleInlineMenuButtonClicked(port),
@ -612,13 +614,13 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* the list and button ports and sets them to null. * the list and button ports and sets them to null.
* *
* @param overlayElement - The overlay element that was closed, either the list or button * @param overlayElement - The overlay element that was closed, either the list or button
* @param sender - The sender of the port message * @param port - The port that sent the message
*/ */
private overlayElementClosed( private overlayElementClosed(
{ overlayElement }: OverlayBackgroundExtensionMessage, { overlayElement }: OverlayBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender, port: chrome.runtime.Port,
) { ) {
if (sender.tab.id !== this.focusedFieldData?.tabId) { if (port.sender.tab.id !== this.focusedFieldData?.tabId) {
this.expiredPorts.forEach((port) => port.disconnect()); this.expiredPorts.forEach((port) => port.disconnect());
this.expiredPorts = []; this.expiredPorts = [];
return; return;
@ -1217,6 +1219,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* @param port - The port that connected to the extension background * @param port - The port that connected to the extension background
*/ */
private handlePortOnConnect = async (port: chrome.runtime.Port) => { private handlePortOnConnect = async (port: chrome.runtime.Port) => {
if (port.name === AutofillOverlayPort.ContentScript) {
port.onMessage.addListener(this.handleContentScriptPortMessage);
return;
}
const isInlineMenuListMessageConnector = port.name === AutofillOverlayPort.ListMessageConnector; const isInlineMenuListMessageConnector = port.name === AutofillOverlayPort.ListMessageConnector;
const isInlineMenuButtonMessageConnector = const isInlineMenuButtonMessageConnector =
port.name === AutofillOverlayPort.ButtonMessageConnector; port.name === AutofillOverlayPort.ButtonMessageConnector;
@ -1293,6 +1300,21 @@ export class OverlayBackground implements OverlayBackgroundInterface {
} }
} }
private handleContentScriptPortMessage = (
message: OverlayPortMessage,
port: chrome.runtime.Port,
) => {
if (port.name !== AutofillOverlayPort.ContentScript) {
return;
}
const handler: CallableFunction | undefined =
this.contentScriptPortMessageHandlers[message.command];
if (handler) {
handler({ message, port });
}
};
/** /**
* Handles messages sent to the overlay list or button ports. * Handles messages sent to the overlay list or button ports.
* *
@ -1300,7 +1322,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* @param port - The port that sent the message * @param port - The port that sent the message
*/ */
private handleOverlayElementPortMessage = ( private handleOverlayElementPortMessage = (
message: OverlayBackgroundExtensionMessage, message: OverlayPortMessage,
port: chrome.runtime.Port, port: chrome.runtime.Port,
) => { ) => {
const tabPortKey = this.portKeyForTab[port.sender.tab.id]; const tabPortKey = this.portKeyForTab[port.sender.tab.id];

View File

@ -33,14 +33,14 @@ class AutofillInit implements AutofillInitInterface {
* CollectAutofillContentService and InsertAutofillContentService classes. * CollectAutofillContentService and InsertAutofillContentService classes.
* *
* @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined.
* @param inlineMenuElements - The inline menu elements, potentially undefined. * @param autofillInlineMenuContentService - The inline menu elements, potentially undefined.
*/ */
constructor( constructor(
autofillOverlayContentService?: AutofillOverlayContentService, autofillOverlayContentService?: AutofillOverlayContentService,
inlineMenuElements?: AutofillInlineMenuContentService, autofillInlineMenuContentService?: AutofillInlineMenuContentService,
) { ) {
this.autofillOverlayContentService = autofillOverlayContentService; this.autofillOverlayContentService = autofillOverlayContentService;
this.autofillInlineMenuContentService = inlineMenuElements; this.autofillInlineMenuContentService = autofillInlineMenuContentService;
this.domElementVisibilityService = new DomElementVisibilityService( this.domElementVisibilityService = new DomElementVisibilityService(
this.autofillInlineMenuContentService, this.autofillInlineMenuContentService,
); );

View File

@ -1,3 +1,4 @@
import { AutofillOverlayPort } from "../enums/autofill-overlay.enum";
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service";
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service";
@ -7,17 +8,19 @@ import AutofillInit from "./autofill-init";
(function (windowContext) { (function (windowContext) {
if (!windowContext.bitwardenAutofillInit) { if (!windowContext.bitwardenAutofillInit) {
const overlayPort = chrome.runtime.connect({ name: AutofillOverlayPort.ContentScript });
const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
const autofillOverlayContentService = new AutofillOverlayContentService( const autofillOverlayContentService = new AutofillOverlayContentService(
overlayPort,
inlineMenuFieldQualificationService, inlineMenuFieldQualificationService,
); );
let inlineMenuElements: AutofillInlineMenuContentService; let autofillInlineMenuContentService: AutofillInlineMenuContentService;
if (globalThis.self === globalThis.top) { if (globalThis.self === globalThis.top) {
inlineMenuElements = new AutofillInlineMenuContentService(); autofillInlineMenuContentService = new AutofillInlineMenuContentService(overlayPort);
} }
windowContext.bitwardenAutofillInit = new AutofillInit( windowContext.bitwardenAutofillInit = new AutofillInit(
autofillOverlayContentService, autofillOverlayContentService,
inlineMenuElements, autofillInlineMenuContentService,
); );
setupAutofillInitDisconnectAction(windowContext); setupAutofillInitDisconnectAction(windowContext);

View File

@ -4,6 +4,7 @@ export const AutofillOverlayElement = {
} as const; } as const;
export const AutofillOverlayPort = { export const AutofillOverlayPort = {
ContentScript: "autofill-overlay-content-script-port",
Button: "autofill-inline-menu-button-port", Button: "autofill-inline-menu-button-port",
ButtonMessageConnector: "autofill-inline-menu-button-message-connector", ButtonMessageConnector: "autofill-inline-menu-button-message-connector",
List: "autofill-inline-menu-list-port", List: "autofill-inline-menu-list-port",

View File

@ -1,14 +1,15 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import AutofillInit from "../../../content/autofill-init"; import AutofillInit from "../../../content/autofill-init";
import { AutofillOverlayElement } from "../../../enums/autofill-overlay.enum"; import { AutofillOverlayElement, AutofillOverlayPort } from "../../../enums/autofill-overlay.enum";
import { createMutationRecordMock } from "../../../spec/autofill-mocks"; import { createMutationRecordMock, createPortSpyMock } from "../../../spec/autofill-mocks";
import { flushPromises, sendMockExtensionMessage } from "../../../spec/testing-utils"; import { flushPromises, sendMockExtensionMessage } from "../../../spec/testing-utils";
import { ElementWithOpId } from "../../../types"; import { ElementWithOpId } from "../../../types";
import { AutofillInlineMenuContentService } from "./autofill-inline-menu-content.service"; import { AutofillInlineMenuContentService } from "./autofill-inline-menu-content.service";
describe("AutofillInlineMenuContentService", () => { describe("AutofillInlineMenuContentService", () => {
let overlayPort: chrome.runtime.Port;
let autofillInlineMenuContentService: AutofillInlineMenuContentService; let autofillInlineMenuContentService: AutofillInlineMenuContentService;
let autofillInit: AutofillInit; let autofillInit: AutofillInit;
let sendExtensionMessageSpy: jest.SpyInstance; let sendExtensionMessageSpy: jest.SpyInstance;
@ -17,7 +18,8 @@ describe("AutofillInlineMenuContentService", () => {
beforeEach(() => { beforeEach(() => {
globalThis.document.body.innerHTML = ""; globalThis.document.body.innerHTML = "";
autofillInlineMenuContentService = new AutofillInlineMenuContentService(); overlayPort = createPortSpyMock(AutofillOverlayPort.ContentScript);
autofillInlineMenuContentService = new AutofillInlineMenuContentService(overlayPort);
autofillInit = new AutofillInit(null, autofillInlineMenuContentService); autofillInit = new AutofillInit(null, autofillInlineMenuContentService);
autofillInit.init(); autofillInit.init();
observeBodyMutationsSpy = jest.spyOn( observeBodyMutationsSpy = jest.spyOn(

View File

@ -41,7 +41,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
checkIsAutofillInlineMenuListVisible: () => this.isInlineMenuListVisible(), checkIsAutofillInlineMenuListVisible: () => this.isInlineMenuListVisible(),
}; };
constructor() { constructor(private port: chrome.runtime.Port) {
this.setupMutationObserver(); this.setupMutationObserver();
} }
@ -115,7 +115,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
if (this.buttonElement) { if (this.buttonElement) {
this.buttonElement.remove(); this.buttonElement.remove();
this.isButtonVisible = false; this.isButtonVisible = false;
void this.sendExtensionMessage("autofillOverlayElementClosed", { this.sendPortMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.Button, overlayElement: AutofillOverlayElement.Button,
}); });
} }
@ -128,7 +128,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
if (this.listElement) { if (this.listElement) {
this.listElement.remove(); this.listElement.remove();
this.isListVisible = false; this.isListVisible = false;
void this.sendExtensionMessage("autofillOverlayElementClosed", { this.sendPortMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.List, overlayElement: AutofillOverlayElement.List,
}); });
} }
@ -421,6 +421,16 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
return false; return false;
} }
/**
* Sends a message through the port to the background script.
*
* @param command - The command to send through the port.
* @param message - The message to send through the port.
*/
private sendPortMessage(command: string, message: Omit<AutofillExtensionMessage, "command">) {
this.port.postMessage({ command, ...message });
}
/** /**
* Disconnects the mutation observers and removes the inline menu elements from the DOM. * Disconnects the mutation observers and removes the inline menu elements from the DOM.
*/ */

View File

@ -6,13 +6,14 @@ import { AutofillOverlayVisibility, EVENTS } from "@bitwarden/common/autofill/co
import AutofillInit from "../content/autofill-init"; import AutofillInit from "../content/autofill-init";
import { import {
AutofillOverlayElement, AutofillOverlayElement,
AutofillOverlayPort,
MAX_SUB_FRAME_DEPTH, MAX_SUB_FRAME_DEPTH,
RedirectFocusDirection, RedirectFocusDirection,
} from "../enums/autofill-overlay.enum"; } from "../enums/autofill-overlay.enum";
import AutofillField from "../models/autofill-field"; import AutofillField from "../models/autofill-field";
import AutofillForm from "../models/autofill-form"; import AutofillForm from "../models/autofill-form";
import AutofillPageDetails from "../models/autofill-page-details"; import AutofillPageDetails from "../models/autofill-page-details";
import { createAutofillFieldMock } from "../spec/autofill-mocks"; import { createAutofillFieldMock, createPortSpyMock } from "../spec/autofill-mocks";
import { flushPromises, postWindowMessage, sendMockExtensionMessage } from "../spec/testing-utils"; import { flushPromises, postWindowMessage, sendMockExtensionMessage } from "../spec/testing-utils";
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types"; import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
@ -25,6 +26,7 @@ const defaultDocumentVisibilityState = document.visibilityState;
describe("AutofillOverlayContentService", () => { describe("AutofillOverlayContentService", () => {
let autofillInit: AutofillInit; let autofillInit: AutofillInit;
let inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; let inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
let overlayPort: chrome.runtime.Port;
let autofillOverlayContentService: AutofillOverlayContentService; let autofillOverlayContentService: AutofillOverlayContentService;
let sendExtensionMessageSpy: jest.SpyInstance; let sendExtensionMessageSpy: jest.SpyInstance;
const sendResponseSpy = jest.fn(); const sendResponseSpy = jest.fn();
@ -39,8 +41,10 @@ describe("AutofillOverlayContentService", () => {
}); });
beforeEach(() => { beforeEach(() => {
overlayPort = createPortSpyMock(AutofillOverlayPort.ContentScript);
inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
autofillOverlayContentService = new AutofillOverlayContentService( autofillOverlayContentService = new AutofillOverlayContentService(
overlayPort,
inlineMenuFieldQualificationService, inlineMenuFieldQualificationService,
); );
autofillInit = new AutofillInit(autofillOverlayContentService); autofillInit = new AutofillInit(autofillOverlayContentService);

View File

@ -79,7 +79,10 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
destroyAutofillInlineMenuListeners: () => this.destroy(), destroyAutofillInlineMenuListeners: () => this.destroy(),
}; };
constructor(private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService) {} constructor(
private port: chrome.runtime.Port,
private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService,
) {}
/** /**
* Initializes the autofill overlay content service by setting up the mutation observers. * Initializes the autofill overlay content service by setting up the mutation observers.

View File

@ -1,8 +1,13 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { AutofillOverlayPort } from "../enums/autofill-overlay.enum";
import AutofillField from "../models/autofill-field"; import AutofillField from "../models/autofill-field";
import AutofillForm from "../models/autofill-form"; import AutofillForm from "../models/autofill-form";
import { createAutofillFieldMock, createAutofillFormMock } from "../spec/autofill-mocks"; import {
createAutofillFieldMock,
createAutofillFormMock,
createPortSpyMock,
} from "../spec/autofill-mocks";
import { mockQuerySelectorAllDefinedCall } from "../spec/testing-utils"; import { mockQuerySelectorAllDefinedCall } from "../spec/testing-utils";
import { import {
ElementWithOpId, ElementWithOpId,
@ -28,9 +33,11 @@ const mockLoginForm = `
const waitForIdleCallback = () => new Promise((resolve) => globalThis.requestIdleCallback(resolve)); const waitForIdleCallback = () => new Promise((resolve) => globalThis.requestIdleCallback(resolve));
describe("CollectAutofillContentService", () => { describe("CollectAutofillContentService", () => {
const overlayPort = createPortSpyMock(AutofillOverlayPort.ContentScript);
const domElementVisibilityService = new DomElementVisibilityService(); const domElementVisibilityService = new DomElementVisibilityService();
const inlineMenuFieldQualificationService = mock<InlineMenuFieldQualificationService>(); const inlineMenuFieldQualificationService = mock<InlineMenuFieldQualificationService>();
const autofillOverlayContentService = new AutofillOverlayContentService( const autofillOverlayContentService = new AutofillOverlayContentService(
overlayPort,
inlineMenuFieldQualificationService, inlineMenuFieldQualificationService,
); );
let collectAutofillContentService: CollectAutofillContentService; let collectAutofillContentService: CollectAutofillContentService;

View File

@ -2,7 +2,9 @@ import { mock } from "jest-mock-extended";
import { EVENTS } from "@bitwarden/common/autofill/constants"; import { EVENTS } from "@bitwarden/common/autofill/constants";
import { AutofillOverlayPort } from "../enums/autofill-overlay.enum";
import AutofillScript, { FillScript, FillScriptActions } from "../models/autofill-script"; import AutofillScript, { FillScript, FillScriptActions } from "../models/autofill-script";
import { createPortSpyMock } from "../spec/autofill-mocks";
import { mockQuerySelectorAllDefinedCall } from "../spec/testing-utils"; import { mockQuerySelectorAllDefinedCall } from "../spec/testing-utils";
import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types"; import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types";
@ -67,9 +69,11 @@ function setMockWindowLocation({
} }
describe("InsertAutofillContentService", () => { describe("InsertAutofillContentService", () => {
const overlayPort = createPortSpyMock(AutofillOverlayPort.ContentScript);
const inlineMenuFieldQualificationService = mock<InlineMenuFieldQualificationService>(); const inlineMenuFieldQualificationService = mock<InlineMenuFieldQualificationService>();
const domElementVisibilityService = new DomElementVisibilityService(); const domElementVisibilityService = new DomElementVisibilityService();
const autofillOverlayContentService = new AutofillOverlayContentService( const autofillOverlayContentService = new AutofillOverlayContentService(
overlayPort,
inlineMenuFieldQualificationService, inlineMenuFieldQualificationService,
); );
const collectAutofillContentService = new CollectAutofillContentService( const collectAutofillContentService = new CollectAutofillContentService(