mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 17:23:37 +00:00
[PM-5189] Reworking extension messages used within autofill init
This commit is contained in:
@@ -326,13 +326,8 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
subFrameOffsetsForTab.set(frameId, null);
|
subFrameOffsetsForTab.set(frameId, null);
|
||||||
void BrowserApi.tabSendMessage(
|
void BrowserApi.tabSendMessage(
|
||||||
tab,
|
tab,
|
||||||
{
|
{ command: "getSubFrameOffsetsFromWindowMessage", subFrameId: frameId },
|
||||||
command: "getSubFrameOffsetsThroughWindowMessaging",
|
{ frameId: frameId },
|
||||||
subFrameId: frameId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
frameId: frameId,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -494,7 +489,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
|
|
||||||
await BrowserApi.tabSendMessage(
|
await BrowserApi.tabSendMessage(
|
||||||
sender.tab,
|
sender.tab,
|
||||||
{ command: "updateInlineMenuElementsPosition", overlayElement },
|
{ command: "appendInlineMenuElementsToDom", overlayElement },
|
||||||
{ frameId: 0 },
|
{ frameId: 0 },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
|
||||||
import { SubFrameOffsetData } from "../../background/abstractions/overlay.background";
|
|
||||||
import AutofillScript from "../../models/autofill-script";
|
import AutofillScript from "../../models/autofill-script";
|
||||||
|
|
||||||
export type AutofillExtensionMessage = {
|
export type AutofillExtensionMessage = {
|
||||||
@@ -19,7 +18,7 @@ export type AutofillExtensionMessage = {
|
|||||||
authStatus?: AuthenticationStatus;
|
authStatus?: AuthenticationStatus;
|
||||||
isFocusingFieldElement?: boolean;
|
isFocusingFieldElement?: boolean;
|
||||||
isOverlayCiphersPopulated?: boolean;
|
isOverlayCiphersPopulated?: boolean;
|
||||||
direction?: "previous" | "next";
|
direction?: "previous" | "next" | "current";
|
||||||
isOpeningFullOverlay?: boolean;
|
isOpeningFullOverlay?: boolean;
|
||||||
forceCloseOverlay?: boolean;
|
forceCloseOverlay?: boolean;
|
||||||
autofillOverlayVisibility?: number;
|
autofillOverlayVisibility?: number;
|
||||||
@@ -33,15 +32,6 @@ export type AutofillExtensionMessageHandlers = {
|
|||||||
collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void;
|
collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void;
|
||||||
collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void;
|
collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void;
|
||||||
fillForm: ({ 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 {
|
export interface AutofillInit {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import AutofillPageDetails from "../models/autofill-page-details";
|
|||||||
import AutofillScript from "../models/autofill-script";
|
import AutofillScript from "../models/autofill-script";
|
||||||
import AutofillOverlayContentService from "../services/autofill-overlay-content.service";
|
import AutofillOverlayContentService from "../services/autofill-overlay-content.service";
|
||||||
import { flushPromises, sendMockExtensionMessage } from "../spec/testing-utils";
|
import { flushPromises, sendMockExtensionMessage } from "../spec/testing-utils";
|
||||||
import { RedirectFocusDirection } from "../utils/autofill-overlay.enum";
|
|
||||||
|
|
||||||
import { AutofillExtensionMessage } from "./abstractions/autofill-init";
|
import { AutofillExtensionMessage } from "./abstractions/autofill-init";
|
||||||
import AutofillInit from "./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", () => {
|
describe("updateIsOverlayCiphersPopulated", () => {
|
||||||
const message = {
|
const message = {
|
||||||
command: "updateIsOverlayCiphersPopulated",
|
command: "updateIsOverlayCiphersPopulated",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { SubFrameOffsetData } from "../background/abstractions/overlay.background";
|
|
||||||
import AutofillPageDetails from "../models/autofill-page-details";
|
import AutofillPageDetails from "../models/autofill-page-details";
|
||||||
import { InlineMenuElements } from "../overlay/abstractions/inline-menu-elements";
|
import { InlineMenuElements } from "../overlay/abstractions/inline-menu-elements";
|
||||||
import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service";
|
import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service";
|
||||||
@@ -24,16 +23,6 @@ class AutofillInit implements AutofillInitInterface {
|
|||||||
collectPageDetails: ({ message }) => this.collectPageDetails(message),
|
collectPageDetails: ({ message }) => this.collectPageDetails(message),
|
||||||
collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true),
|
collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true),
|
||||||
fillForm: ({ message }) => this.fillForm(message),
|
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.domElementVisibilityService,
|
||||||
this.collectAutofillContentService,
|
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
|
* Blurs the most recent overlay field and removes the overlay. Used
|
||||||
* in cases where the background unlock or vault item reprompt popout
|
* in cases where the background unlock or vault item reprompt popout
|
||||||
@@ -221,117 +158,7 @@ class AutofillInit implements AutofillInitInterface {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.autofillOverlayContentService.blurMostRecentOverlayField();
|
this.autofillOverlayContentService.blurMostRecentOverlayField(true);
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"*",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -364,9 +191,7 @@ class AutofillInit implements AutofillInitInterface {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
void Promise.resolve(messageResponse).then((response) => sendResponse(response));
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
Promise.resolve(messageResponse).then((response) => sendResponse(response));
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AutofillExtensionMessageParam } from "../../content/abstractions/autofi
|
|||||||
export type InlineMenuExtensionMessageHandlers = {
|
export type InlineMenuExtensionMessageHandlers = {
|
||||||
[key: string]: CallableFunction;
|
[key: string]: CallableFunction;
|
||||||
closeInlineMenu: ({ message }: AutofillExtensionMessageParam) => void;
|
closeInlineMenu: ({ message }: AutofillExtensionMessageParam) => void;
|
||||||
updateInlineMenuElementsPosition: ({ message }: AutofillExtensionMessageParam) => Promise<void>;
|
appendInlineMenuElementsToDom: ({ message }: AutofillExtensionMessageParam) => Promise<void>;
|
||||||
toggleInlineMenuHidden: ({ message }: AutofillExtensionMessageParam) => void;
|
toggleInlineMenuHidden: ({ message }: AutofillExtensionMessageParam) => void;
|
||||||
checkIsInlineMenuButtonVisible: () => boolean;
|
checkIsInlineMenuButtonVisible: () => boolean;
|
||||||
checkIsInlineMenuListVisible: () => boolean;
|
checkIsInlineMenuListVisible: () => boolean;
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ export class InlineMenuElements implements InlineMenuElementsInterface {
|
|||||||
};
|
};
|
||||||
private readonly _extensionMessageHandlers: InlineMenuExtensionMessageHandlers = {
|
private readonly _extensionMessageHandlers: InlineMenuExtensionMessageHandlers = {
|
||||||
closeInlineMenu: ({ message }) => this.removeInlineMenu(message),
|
closeInlineMenu: ({ message }) => this.removeInlineMenu(message),
|
||||||
updateInlineMenuElementsPosition: ({ message }) =>
|
appendInlineMenuElementsToDom: ({ message }) => this.appendInlineMenuElements(message),
|
||||||
this.updateInlineMenuElementsPosition(message),
|
|
||||||
toggleInlineMenuHidden: ({ message }) =>
|
toggleInlineMenuHidden: ({ message }) =>
|
||||||
this.toggleInlineMenuHidden(message.isInlineMenuHidden),
|
this.toggleInlineMenuHidden(message.isInlineMenuHidden),
|
||||||
checkIsInlineMenuButtonVisible: () => this.isButtonVisible,
|
checkIsInlineMenuButtonVisible: () => this.isButtonVisible,
|
||||||
@@ -125,25 +124,25 @@ export class InlineMenuElements implements InlineMenuElementsInterface {
|
|||||||
/**
|
/**
|
||||||
* Updates the position of both the overlay button and overlay list.
|
* 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) {
|
if (overlayElement === AutofillOverlayElement.Button) {
|
||||||
return this.updateButtonPosition();
|
return this.appendButtonElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.updateListPosition();
|
return this.appendListElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the position of the overlay button.
|
* Updates the position of the overlay button.
|
||||||
*/
|
*/
|
||||||
private async updateButtonPosition(): Promise<void> {
|
private async appendButtonElement(): Promise<void> {
|
||||||
if (!this.buttonElement) {
|
if (!this.buttonElement) {
|
||||||
this.createButton();
|
this.createButton();
|
||||||
this.updateCustomElementDefaultStyles(this.buttonElement);
|
this.updateCustomElementDefaultStyles(this.buttonElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isButtonVisible) {
|
if (!this.isButtonVisible) {
|
||||||
this.appendOverlayElementToBody(this.buttonElement);
|
this.appendInlineMenuElementToBody(this.buttonElement);
|
||||||
this.isButtonVisible = true;
|
this.isButtonVisible = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,14 +150,14 @@ export class InlineMenuElements implements InlineMenuElementsInterface {
|
|||||||
/**
|
/**
|
||||||
* Updates the position of the overlay list.
|
* Updates the position of the overlay list.
|
||||||
*/
|
*/
|
||||||
private async updateListPosition(): Promise<void> {
|
private async appendListElement(): Promise<void> {
|
||||||
if (!this.listElement) {
|
if (!this.listElement) {
|
||||||
this.createList();
|
this.createList();
|
||||||
this.updateCustomElementDefaultStyles(this.listElement);
|
this.updateCustomElementDefaultStyles(this.listElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isListVisible) {
|
if (!this.isListVisible) {
|
||||||
this.appendOverlayElementToBody(this.listElement);
|
this.appendInlineMenuElementToBody(this.listElement);
|
||||||
this.isListVisible = true;
|
this.isListVisible = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,7 +169,7 @@ export class InlineMenuElements implements InlineMenuElementsInterface {
|
|||||||
*
|
*
|
||||||
* @param element - The overlay element to append to the body element.
|
* @param element - The overlay element to append to the body element.
|
||||||
*/
|
*/
|
||||||
private appendOverlayElementToBody(element: HTMLElement) {
|
private appendInlineMenuElementToBody(element: HTMLElement) {
|
||||||
this.observeBodyElement();
|
this.observeBodyElement();
|
||||||
globalThis.document.body.appendChild(element);
|
globalThis.document.body.appendChild(element);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
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 AutofillField from "../../models/autofill-field";
|
||||||
import { ElementWithOpId, FormFieldElement } from "../../types";
|
import { ElementWithOpId, FormFieldElement } from "../../types";
|
||||||
|
|
||||||
@@ -11,7 +13,16 @@ export type OpenAutofillOverlayOptions = {
|
|||||||
|
|
||||||
export type AutofillOverlayContentExtensionMessageHandlers = {
|
export type AutofillOverlayContentExtensionMessageHandlers = {
|
||||||
[key: string]: CallableFunction;
|
[key: string]: CallableFunction;
|
||||||
|
openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void;
|
||||||
|
addNewVaultItemFromOverlay: () => void;
|
||||||
blurMostRecentOverlayField: () => 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 {
|
export interface AutofillOverlayContentService {
|
||||||
@@ -31,8 +42,7 @@ export interface AutofillOverlayContentService {
|
|||||||
// removeAutofillOverlayButton(): void;
|
// removeAutofillOverlayButton(): void;
|
||||||
// removeAutofillOverlayList(): void;
|
// removeAutofillOverlayList(): void;
|
||||||
addNewVaultItem(): void;
|
addNewVaultItem(): void;
|
||||||
redirectOverlayFocusOut(direction: "previous" | "next"): void;
|
|
||||||
focusMostRecentOverlayField(): void;
|
focusMostRecentOverlayField(): void;
|
||||||
blurMostRecentOverlayField(): void;
|
blurMostRecentOverlayField(isRemovingOverlay?: boolean): void;
|
||||||
destroy(): void;
|
destroy(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { FocusableElement, tabbable } from "tabbable";
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { EVENTS, AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
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 AutofillField from "../models/autofill-field";
|
||||||
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
||||||
import { elementIsFillableFormField, sendExtensionMessage } from "../utils";
|
import { elementIsFillableFormField, sendExtensionMessage } from "../utils";
|
||||||
@@ -36,7 +40,17 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
|
|||||||
private autofillFieldKeywordsMap: WeakMap<AutofillField, string> = new WeakMap();
|
private autofillFieldKeywordsMap: WeakMap<AutofillField, string> = new WeakMap();
|
||||||
private eventHandlersMemo: { [key: string]: EventListener } = {};
|
private eventHandlersMemo: { [key: string]: EventListener } = {};
|
||||||
readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = {
|
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.
|
* Removes focus from the most recently focused field element.
|
||||||
*/
|
*/
|
||||||
blurMostRecentOverlayField() {
|
blurMostRecentOverlayField(isRemovingOverlay: boolean = false) {
|
||||||
this.mostRecentlyFocusedField?.blur();
|
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
|
* either previous or next in the tab order. If the direction is current, the most
|
||||||
* recently focused field will be focused.
|
* 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 (
|
if (
|
||||||
|
!data?.direction ||
|
||||||
!this.mostRecentlyFocusedField ||
|
!this.mostRecentlyFocusedField ||
|
||||||
(await this.sendExtensionMessage("checkIsInlineMenuListVisible")) !== true
|
(await this.sendExtensionMessage("checkIsInlineMenuListVisible")) !== true
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { direction } = data;
|
||||||
|
|
||||||
if (direction === RedirectFocusDirection.Current) {
|
if (direction === RedirectFocusDirection.Current) {
|
||||||
this.focusMostRecentOverlayField();
|
this.focusMostRecentOverlayField();
|
||||||
setTimeout(() => void this.sendExtensionMessage("closeAutofillOverlay"), 100);
|
setTimeout(() => void this.sendExtensionMessage("closeAutofillOverlay"), 100);
|
||||||
@@ -573,7 +594,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
|
|||||||
private async updateMostRecentlyFocusedField(
|
private async updateMostRecentlyFocusedField(
|
||||||
formFieldElement: ElementWithOpId<FormFieldElement>,
|
formFieldElement: ElementWithOpId<FormFieldElement>,
|
||||||
) {
|
) {
|
||||||
if (!elementIsFillableFormField(formFieldElement)) {
|
if (!formFieldElement || !elementIsFillableFormField(formFieldElement)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,6 +802,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
|
|||||||
* overlay elements.
|
* overlay elements.
|
||||||
*/
|
*/
|
||||||
private setupGlobalEventListeners = () => {
|
private setupGlobalEventListeners = () => {
|
||||||
|
globalThis.addEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent);
|
||||||
globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent);
|
globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent);
|
||||||
globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
|
globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
|
||||||
this.setOverlayRepositionEventListeners();
|
this.setOverlayRepositionEventListeners();
|
||||||
@@ -815,6 +837,112 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
|
|||||||
return documentRoot?.activeElement;
|
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
|
* Destroys the autofill overlay content service. This method will
|
||||||
* disconnect the mutation observers and remove all event listeners.
|
* disconnect the mutation observers and remove all event listeners.
|
||||||
|
|||||||
Reference in New Issue
Block a user