1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-06 23:51:28 +01:00

[PM-5189] Reworking extension messages used within autofill init

This commit is contained in:
Cesar Gonzalez 2024-04-07 17:11:27 -05:00
parent ef716ee728
commit e9c351f7f3
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
8 changed files with 162 additions and 242 deletions

View File

@ -326,13 +326,8 @@ class OverlayBackground implements OverlayBackgroundInterface {
subFrameOffsetsForTab.set(frameId, null);
void BrowserApi.tabSendMessage(
tab,
{
command: "getSubFrameOffsetsThroughWindowMessaging",
subFrameId: frameId,
},
{
frameId: frameId,
},
{ command: "getSubFrameOffsetsFromWindowMessage", subFrameId: frameId },
{ frameId: frameId },
);
return;
}
@ -494,7 +489,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
await BrowserApi.tabSendMessage(
sender.tab,
{ command: "updateInlineMenuElementsPosition", overlayElement },
{ command: "appendInlineMenuElementsToDom", overlayElement },
{ frameId: 0 },
);

View File

@ -1,6 +1,5 @@
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { SubFrameOffsetData } from "../../background/abstractions/overlay.background";
import AutofillScript from "../../models/autofill-script";
export type AutofillExtensionMessage = {
@ -19,7 +18,7 @@ export type AutofillExtensionMessage = {
authStatus?: AuthenticationStatus;
isFocusingFieldElement?: boolean;
isOverlayCiphersPopulated?: boolean;
direction?: "previous" | "next";
direction?: "previous" | "next" | "current";
isOpeningFullOverlay?: boolean;
forceCloseOverlay?: boolean;
autofillOverlayVisibility?: number;
@ -33,15 +32,6 @@ export type AutofillExtensionMessageHandlers = {
collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void;
collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void;
fillForm: ({ message }: AutofillExtensionMessageParam) => void;
openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void;
addNewVaultItemFromOverlay: () => void;
redirectOverlayFocusOut: ({ message }: AutofillExtensionMessageParam) => void;
updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void;
bgUnlockPopoutOpened: () => void;
bgVaultItemRepromptPopoutOpened: () => void;
updateAutofillOverlayVisibility: ({ message }: AutofillExtensionMessageParam) => void;
getSubFrameOffsets: ({ message }: AutofillExtensionMessageParam) => Promise<SubFrameOffsetData>;
getSubFrameOffsetsThroughWindowMessaging: ({ message }: AutofillExtensionMessageParam) => void;
};
export interface AutofillInit {

View File

@ -7,7 +7,6 @@ import AutofillPageDetails from "../models/autofill-page-details";
import AutofillScript from "../models/autofill-script";
import AutofillOverlayContentService from "../services/autofill-overlay-content.service";
import { flushPromises, sendMockExtensionMessage } from "../spec/testing-utils";
import { RedirectFocusDirection } from "../utils/autofill-overlay.enum";
import { AutofillExtensionMessage } from "./abstractions/autofill-init";
import AutofillInit from "./autofill-init";
@ -422,32 +421,6 @@ describe("AutofillInit", () => {
});
});
describe("redirectOverlayFocusOut", () => {
const message = {
command: "redirectOverlayFocusOut",
data: {
direction: RedirectFocusDirection.Next,
},
};
it("ignores the message to redirect focus if the autofillOverlayContentService does not exist", () => {
const newAutofillInit = new AutofillInit(undefined);
newAutofillInit.init();
sendMockExtensionMessage(message);
expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined);
});
it("redirects the overlay focus", () => {
sendMockExtensionMessage(message);
expect(
autofillInit["autofillOverlayContentService"].redirectOverlayFocusOut,
).toHaveBeenCalledWith(message.data.direction);
});
});
describe("updateIsOverlayCiphersPopulated", () => {
const message = {
command: "updateIsOverlayCiphersPopulated",

View File

@ -1,4 +1,3 @@
import { SubFrameOffsetData } from "../background/abstractions/overlay.background";
import AutofillPageDetails from "../models/autofill-page-details";
import { InlineMenuElements } from "../overlay/abstractions/inline-menu-elements";
import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service";
@ -24,16 +23,6 @@ class AutofillInit implements AutofillInitInterface {
collectPageDetails: ({ message }) => this.collectPageDetails(message),
collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true),
fillForm: ({ message }) => this.fillForm(message),
openAutofillOverlay: ({ message }) => this.openAutofillOverlay(message),
addNewVaultItemFromOverlay: () => this.addNewVaultItemFromOverlay(),
redirectOverlayFocusOut: ({ message }) => this.redirectOverlayFocusOut(message),
updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message),
bgUnlockPopoutOpened: () => this.blurAndRemoveOverlay(),
bgVaultItemRepromptPopoutOpened: () => this.blurAndRemoveOverlay(),
updateAutofillOverlayVisibility: ({ message }) => this.updateAutofillOverlayVisibility(message),
getSubFrameOffsets: ({ message }) => this.getSubFrameOffsets(message),
getSubFrameOffsetsThroughWindowMessaging: ({ message }) =>
this.getSubFrameOffsetsThroughWindowMessaging(message),
};
/**
@ -72,45 +61,6 @@ class AutofillInit implements AutofillInitInterface {
this.domElementVisibilityService,
this.collectAutofillContentService,
);
window.addEventListener("message", (event) => {
// if (event.source !== window) {
// return;
// }
if (event.data.command === "calculateSubFramePositioning") {
const subFrameData = event.data.subFrameData;
let subFrameOffsets: SubFrameOffsetData;
const iframes = document.querySelectorAll("iframe");
for (let i = 0; i < iframes.length; i++) {
if (iframes[i].contentWindow === event.source) {
const iframeElement = iframes[i];
subFrameOffsets = this.calculateSubFrameOffsets(
iframeElement,
subFrameData.url,
subFrameData.frameId,
);
subFrameData.top += subFrameOffsets.top;
subFrameData.left += subFrameOffsets.left;
break;
}
}
if (globalThis.window.self !== globalThis.window.top) {
globalThis.parent.postMessage(
{ command: "calculateSubFramePositioning", subFrameData },
"*",
);
return;
}
void sendExtensionMessage("updateSubFrameData", {
subFrameData,
});
}
});
}
/**
@ -198,19 +148,6 @@ class AutofillInit implements AutofillInitInterface {
);
}
/**
* Opens the autofill overlay.
*
* @param data - The extension message data.
*/
private openAutofillOverlay({ data }: AutofillExtensionMessage) {
if (!this.autofillOverlayContentService) {
return;
}
this.autofillOverlayContentService.openAutofillOverlay(data);
}
/**
* Blurs the most recent overlay field and removes the overlay. Used
* in cases where the background unlock or vault item reprompt popout
@ -221,117 +158,7 @@ class AutofillInit implements AutofillInitInterface {
return;
}
this.autofillOverlayContentService.blurMostRecentOverlayField();
void sendExtensionMessage("closeAutofillOverlay");
}
/**
* Adds a new vault item from the overlay.
*/
private addNewVaultItemFromOverlay() {
if (!this.autofillOverlayContentService) {
return;
}
this.autofillOverlayContentService.addNewVaultItem();
}
/**
* Redirects the overlay focus out of an overlay iframe.
*
* @param data - Contains the direction to redirect the focus.
*/
private redirectOverlayFocusOut({ data }: AutofillExtensionMessage) {
if (!this.autofillOverlayContentService) {
return;
}
this.autofillOverlayContentService.redirectOverlayFocusOut(data?.direction);
}
/**
* Updates whether the current tab has ciphers that can populate the overlay list
*
* @param data - Contains the isOverlayCiphersPopulated value
*
*/
private updateIsOverlayCiphersPopulated({ data }: AutofillExtensionMessage) {
if (!this.autofillOverlayContentService) {
return;
}
this.autofillOverlayContentService.isOverlayCiphersPopulated = Boolean(
data?.isOverlayCiphersPopulated,
);
}
/**
* Updates the autofill overlay visibility.
*
* @param data - Contains the autoFillOverlayVisibility value
*/
private updateAutofillOverlayVisibility({ data }: AutofillExtensionMessage) {
if (!this.autofillOverlayContentService || isNaN(data?.autofillOverlayVisibility)) {
return;
}
this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility;
}
private async getSubFrameOffsets(
message: AutofillExtensionMessage,
): Promise<SubFrameOffsetData | null> {
const { subFrameUrl } = message;
const subFrameUrlWithoutTrailingSlash = subFrameUrl?.replace(/\/$/, "");
let iframeElement: HTMLIFrameElement | null = null;
const iframeElements = document.querySelectorAll(
`iframe[src="${subFrameUrl}"], iframe[src="${subFrameUrlWithoutTrailingSlash}"]`,
) as NodeListOf<HTMLIFrameElement>;
if (iframeElements.length === 1) {
iframeElement = iframeElements[0];
}
if (!iframeElement) {
return null;
}
return this.calculateSubFrameOffsets(iframeElement, subFrameUrl);
}
private calculateSubFrameOffsets(
iframeElement: HTMLIFrameElement,
subFrameUrl?: string,
frameId?: number,
): SubFrameOffsetData {
const iframeRect = iframeElement.getBoundingClientRect();
const iframeStyles = globalThis.getComputedStyle(iframeElement);
const paddingLeft = parseInt(iframeStyles.getPropertyValue("padding-left"));
const paddingTop = parseInt(iframeStyles.getPropertyValue("padding-top"));
const borderWidthLeft = parseInt(iframeStyles.getPropertyValue("border-left-width"));
const borderWidthTop = parseInt(iframeStyles.getPropertyValue("border-top-width"));
return {
url: subFrameUrl,
frameId,
top: iframeRect.top + paddingTop + borderWidthTop,
left: iframeRect.left + paddingLeft + borderWidthLeft,
};
}
private getSubFrameOffsetsThroughWindowMessaging(message: any) {
globalThis.parent.postMessage(
{
command: "calculateSubFramePositioning",
subFrameData: {
url: window.location.href,
frameId: message.subFrameId,
left: 0,
top: 0,
},
},
"*",
);
this.autofillOverlayContentService.blurMostRecentOverlayField(true);
}
/**
@ -364,9 +191,7 @@ class AutofillInit implements AutofillInitInterface {
return;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Promise.resolve(messageResponse).then((response) => sendResponse(response));
void Promise.resolve(messageResponse).then((response) => sendResponse(response));
return true;
};

View File

@ -3,7 +3,7 @@ import { AutofillExtensionMessageParam } from "../../content/abstractions/autofi
export type InlineMenuExtensionMessageHandlers = {
[key: string]: CallableFunction;
closeInlineMenu: ({ message }: AutofillExtensionMessageParam) => void;
updateInlineMenuElementsPosition: ({ message }: AutofillExtensionMessageParam) => Promise<void>;
appendInlineMenuElementsToDom: ({ message }: AutofillExtensionMessageParam) => Promise<void>;
toggleInlineMenuHidden: ({ message }: AutofillExtensionMessageParam) => void;
checkIsInlineMenuButtonVisible: () => boolean;
checkIsInlineMenuListVisible: () => boolean;

View File

@ -36,8 +36,7 @@ export class InlineMenuElements implements InlineMenuElementsInterface {
};
private readonly _extensionMessageHandlers: InlineMenuExtensionMessageHandlers = {
closeInlineMenu: ({ message }) => this.removeInlineMenu(message),
updateInlineMenuElementsPosition: ({ message }) =>
this.updateInlineMenuElementsPosition(message),
appendInlineMenuElementsToDom: ({ message }) => this.appendInlineMenuElements(message),
toggleInlineMenuHidden: ({ message }) =>
this.toggleInlineMenuHidden(message.isInlineMenuHidden),
checkIsInlineMenuButtonVisible: () => this.isButtonVisible,
@ -125,25 +124,25 @@ export class InlineMenuElements implements InlineMenuElementsInterface {
/**
* Updates the position of both the overlay button and overlay list.
*/
private async updateInlineMenuElementsPosition({ overlayElement }: AutofillExtensionMessage) {
private async appendInlineMenuElements({ overlayElement }: AutofillExtensionMessage) {
if (overlayElement === AutofillOverlayElement.Button) {
return this.updateButtonPosition();
return this.appendButtonElement();
}
return this.updateListPosition();
return this.appendListElement();
}
/**
* Updates the position of the overlay button.
*/
private async updateButtonPosition(): Promise<void> {
private async appendButtonElement(): Promise<void> {
if (!this.buttonElement) {
this.createButton();
this.updateCustomElementDefaultStyles(this.buttonElement);
}
if (!this.isButtonVisible) {
this.appendOverlayElementToBody(this.buttonElement);
this.appendInlineMenuElementToBody(this.buttonElement);
this.isButtonVisible = true;
}
}
@ -151,14 +150,14 @@ export class InlineMenuElements implements InlineMenuElementsInterface {
/**
* Updates the position of the overlay list.
*/
private async updateListPosition(): Promise<void> {
private async appendListElement(): Promise<void> {
if (!this.listElement) {
this.createList();
this.updateCustomElementDefaultStyles(this.listElement);
}
if (!this.isListVisible) {
this.appendOverlayElementToBody(this.listElement);
this.appendInlineMenuElementToBody(this.listElement);
this.isListVisible = true;
}
}
@ -170,7 +169,7 @@ export class InlineMenuElements implements InlineMenuElementsInterface {
*
* @param element - The overlay element to append to the body element.
*/
private appendOverlayElementToBody(element: HTMLElement) {
private appendInlineMenuElementToBody(element: HTMLElement) {
this.observeBodyElement();
globalThis.document.body.appendChild(element);
}

View File

@ -1,5 +1,7 @@
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { SubFrameOffsetData } from "../../background/abstractions/overlay.background";
import { AutofillExtensionMessageParam } from "../../content/abstractions/autofill-init";
import AutofillField from "../../models/autofill-field";
import { ElementWithOpId, FormFieldElement } from "../../types";
@ -11,7 +13,16 @@ export type OpenAutofillOverlayOptions = {
export type AutofillOverlayContentExtensionMessageHandlers = {
[key: string]: CallableFunction;
openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void;
addNewVaultItemFromOverlay: () => void;
blurMostRecentOverlayField: () => void;
bgUnlockPopoutOpened: () => void;
bgVaultItemRepromptPopoutOpened: () => void;
redirectOverlayFocusOut: ({ message }: AutofillExtensionMessageParam) => void;
updateAutofillOverlayVisibility: ({ message }: AutofillExtensionMessageParam) => void;
updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void;
getSubFrameOffsets: ({ message }: AutofillExtensionMessageParam) => Promise<SubFrameOffsetData>;
getSubFrameOffsetsFromWindowMessage: ({ message }: AutofillExtensionMessageParam) => void;
};
export interface AutofillOverlayContentService {
@ -31,8 +42,7 @@ export interface AutofillOverlayContentService {
// removeAutofillOverlayButton(): void;
// removeAutofillOverlayList(): void;
addNewVaultItem(): void;
redirectOverlayFocusOut(direction: "previous" | "next"): void;
focusMostRecentOverlayField(): void;
blurMostRecentOverlayField(): void;
blurMostRecentOverlayField(isRemovingOverlay?: boolean): void;
destroy(): void;
}

View File

@ -5,7 +5,11 @@ import { FocusableElement, tabbable } from "tabbable";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EVENTS, AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { FocusedFieldData } from "../background/abstractions/overlay.background";
import {
FocusedFieldData,
SubFrameOffsetData,
} from "../background/abstractions/overlay.background";
import { AutofillExtensionMessage } from "../content/abstractions/autofill-init";
import AutofillField from "../models/autofill-field";
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
import { elementIsFillableFormField, sendExtensionMessage } from "../utils";
@ -36,7 +40,17 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
private autofillFieldKeywordsMap: WeakMap<AutofillField, string> = new WeakMap();
private eventHandlersMemo: { [key: string]: EventListener } = {};
readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = {
blurMostRecentOverlayField: () => this.blurMostRecentOverlayField,
openAutofillOverlay: ({ message }) => this.openAutofillOverlay(message.data),
addNewVaultItemFromOverlay: () => this.addNewVaultItem(),
blurMostRecentOverlayField: () => this.blurMostRecentOverlayField(),
bgUnlockPopoutOpened: () => this.blurMostRecentOverlayField(true),
bgVaultItemRepromptPopoutOpened: () => this.blurMostRecentOverlayField(true),
redirectOverlayFocusOut: ({ message }) => this.redirectOverlayFocusOut(message),
updateAutofillOverlayVisibility: ({ message }) => this.updateAutofillOverlayVisibility(message),
updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message),
getSubFrameOffsets: ({ message }) => this.getSubFrameOffsets(message),
getSubFrameOffsetsFromWindowMessage: ({ message }) =>
this.getSubFrameOffsetsFromWindowMessage(message),
};
/**
@ -135,8 +149,12 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
/**
* Removes focus from the most recently focused field element.
*/
blurMostRecentOverlayField() {
blurMostRecentOverlayField(isRemovingOverlay: boolean = false) {
this.mostRecentlyFocusedField?.blur();
if (isRemovingOverlay) {
void sendExtensionMessage("closeAutofillOverlay");
}
}
/**
@ -163,16 +181,19 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
* either previous or next in the tab order. If the direction is current, the most
* recently focused field will be focused.
*
* @param direction - The direction to redirect the focus.
* @param data - Contains the direction to redirect the focus.
*/
async redirectOverlayFocusOut(direction: string) {
async redirectOverlayFocusOut({ data }: AutofillExtensionMessage) {
if (
!data?.direction ||
!this.mostRecentlyFocusedField ||
(await this.sendExtensionMessage("checkIsInlineMenuListVisible")) !== true
) {
return;
}
const { direction } = data;
if (direction === RedirectFocusDirection.Current) {
this.focusMostRecentOverlayField();
setTimeout(() => void this.sendExtensionMessage("closeAutofillOverlay"), 100);
@ -573,7 +594,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
private async updateMostRecentlyFocusedField(
formFieldElement: ElementWithOpId<FormFieldElement>,
) {
if (!elementIsFillableFormField(formFieldElement)) {
if (!formFieldElement || !elementIsFillableFormField(formFieldElement)) {
return;
}
@ -781,6 +802,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
* overlay elements.
*/
private setupGlobalEventListeners = () => {
globalThis.addEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent);
globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent);
globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
this.setOverlayRepositionEventListeners();
@ -815,6 +837,112 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
return documentRoot?.activeElement;
}
private async getSubFrameOffsets(
message: AutofillExtensionMessage,
): Promise<SubFrameOffsetData | null> {
const { subFrameUrl } = message;
const subFrameUrlWithoutTrailingSlash = subFrameUrl?.replace(/\/$/, "");
let iframeElement: HTMLIFrameElement | null = null;
const iframeElements = document.querySelectorAll(
`iframe[src="${subFrameUrl}"], iframe[src="${subFrameUrlWithoutTrailingSlash}"]`,
) as NodeListOf<HTMLIFrameElement>;
if (iframeElements.length === 1) {
iframeElement = iframeElements[0];
}
if (!iframeElement) {
return null;
}
return this.calculateSubFrameOffsets(iframeElement, subFrameUrl);
}
private calculateSubFrameOffsets(
iframeElement: HTMLIFrameElement,
subFrameUrl?: string,
frameId?: number,
): SubFrameOffsetData {
const iframeRect = iframeElement.getBoundingClientRect();
const iframeStyles = globalThis.getComputedStyle(iframeElement);
const paddingLeft = parseInt(iframeStyles.getPropertyValue("padding-left"));
const paddingTop = parseInt(iframeStyles.getPropertyValue("padding-top"));
const borderWidthLeft = parseInt(iframeStyles.getPropertyValue("border-left-width"));
const borderWidthTop = parseInt(iframeStyles.getPropertyValue("border-top-width"));
return {
url: subFrameUrl,
frameId,
top: iframeRect.top + paddingTop + borderWidthTop,
left: iframeRect.left + paddingLeft + borderWidthLeft,
};
}
private getSubFrameOffsetsFromWindowMessage(message: any) {
globalThis.parent.postMessage(
{
command: "calculateSubFramePositioning",
subFrameData: {
url: window.location.href,
frameId: message.subFrameId,
left: 0,
top: 0,
},
},
"*",
);
}
private handleWindowMessageEvent = (event: MessageEvent) => {
if (event.data?.command !== "calculateSubFramePositioning") {
return;
}
this.calculateSubFramePositioning(event);
};
private calculateSubFramePositioning = (event: MessageEvent) => {
const subFrameData = event.data.subFrameData;
let subFrameOffsets: SubFrameOffsetData;
const iframes = document.querySelectorAll("iframe");
for (let i = 0; i < iframes.length; i++) {
if (iframes[i].contentWindow === event.source) {
const iframeElement = iframes[i];
subFrameOffsets = this.calculateSubFrameOffsets(
iframeElement,
subFrameData.url,
subFrameData.frameId,
);
subFrameData.top += subFrameOffsets.top;
subFrameData.left += subFrameOffsets.left;
break;
}
}
if (globalThis.window.self !== globalThis.window.top) {
globalThis.parent.postMessage({ command: "calculateSubFramePositioning", subFrameData }, "*");
return;
}
void sendExtensionMessage("updateSubFrameData", {
subFrameData,
});
};
private updateAutofillOverlayVisibility({ data }: AutofillExtensionMessage) {
if (isNaN(data?.autofillOverlayVisibility)) {
return;
}
this.autofillOverlayVisibility = data.autofillOverlayVisibility;
}
private updateIsOverlayCiphersPopulated({ data }: AutofillExtensionMessage) {
this.isOverlayCiphersPopulated = Boolean(data?.isOverlayCiphersPopulated);
}
/**
* Destroys the autofill overlay content service. This method will
* disconnect the mutation observers and remove all event listeners.