1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-25 21:51:30 +01:00

[PM-5189] Reworking how we handle updating ciphers within nested sub frames

This commit is contained in:
Cesar Gonzalez 2024-06-17 07:49:17 -05:00
parent ede74bc72d
commit aaa585c992
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
8 changed files with 304 additions and 347 deletions

View File

@ -772,12 +772,12 @@ export default class NotificationBackground {
) => {
const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command];
if (!handler) {
return;
return null;
}
const messageResponse = handler({ message, sender });
if (typeof messageResponse === "undefined") {
return;
return null;
}
Promise.resolve(messageResponse)

View File

@ -394,7 +394,7 @@ describe("OverlayBackground", () => {
it("triggers an update of the inline menu position after rebuilding sub frames", async () => {
jest.useFakeTimers();
overlayBackground["updateInlineMenuPositionTimeout"] = setTimeout(jest.fn, 650);
overlayBackground["delayedUpdateInlineMenuPositionTimeout"] = setTimeout(jest.fn, 650);
const sender = mock<chrome.runtime.MessageSender>({ tab, frameId: middleFrameId });
jest.spyOn(overlayBackground as any, "updateInlineMenuPositionAfterSubFrameRebuild");

View File

@ -63,7 +63,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private inlineMenuCiphers: Map<string, CipherView> = new Map();
private inlineMenuPageTranslations: Record<string, string>;
private inlineMenuFadeInTimeout: number | NodeJS.Timeout;
private updateInlineMenuPositionTimeout: number | NodeJS.Timeout;
private delayedUpdateInlineMenuPositionTimeout: number | NodeJS.Timeout;
private delayedCloseTimeout: number | NodeJS.Timeout;
private focusedFieldData: FocusedFieldData;
private isFieldCurrentlyFocused: boolean = false;
@ -252,9 +252,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
};
if (pageDetails.frameId !== 0 && pageDetails.details.fields.length) {
void this.buildSubFrameOffsets(pageDetails.tab, pageDetails.frameId, pageDetails.details.url);
void this.buildSubFrameOffsets(sender, pageDetails.details.url);
void BrowserApi.tabSendMessage(pageDetails.tab, {
command: "setupAutofillInlineMenuReflowObserver",
command: "setupRebuildSubFrameOffsetsListeners",
});
}
@ -292,6 +292,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id];
if (subFrameOffsetsForTab) {
subFrameOffsetsForTab.set(message.subFrameData.frameId, message.subFrameData);
this.delayedUpdateInlineMenuPosition(sender);
}
}
@ -299,12 +300,12 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* Builds the offset data for a sub frame of a tab. The offset data is used
* to calculate the position of the inline menu list and button.
*
* @param tab - The tab that the sub frame is associated with
* @param frameId - The frame ID of the sub frame
* @param sender - The sender of the message
* @param url - The URL of the sub frame
*/
private async buildSubFrameOffsets(tab: chrome.tabs.Tab, frameId: number, url: string) {
private async buildSubFrameOffsets(sender: chrome.runtime.MessageSender, url: string) {
let subFrameDepth = 0;
const { tab, frameId } = sender;
const tabId = tab.id;
let subFrameOffsetsForTab = this.subFrameOffsetsForTab[tabId];
if (!subFrameOffsetsForTab) {
@ -358,6 +359,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
}
subFrameOffsetsForTab.set(frameId, subFrameData);
this.delayedUpdateInlineMenuPosition(sender);
}
/**
@ -392,12 +394,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
if (subFrameOffsetsForTab) {
const tabFrameIds = Array.from(subFrameOffsetsForTab.keys());
for (const frameId of tabFrameIds) {
// if (frameId === sender.frameId) {
// continue;
// }
subFrameOffsetsForTab.delete(frameId);
await this.buildSubFrameOffsets(sender.tab, frameId, sender.url);
await this.buildSubFrameOffsets(sender, sender.url);
}
}
}
@ -416,14 +414,15 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return;
}
this.clearUpdateInlineMenuPositionTimeout();
await this.rebuildSubFrameOffsets(sender);
}
this.updateInlineMenuPositionTimeout = globalThis.setTimeout(
() => this.updateInlineMenuPositionAfterSubFrameRebuild(sender),
650,
);
private delayedUpdateInlineMenuPosition(sender: chrome.runtime.MessageSender) {
this.clearDelayedUpdateInlineMenuPositionTimeout();
this.delayedUpdateInlineMenuPositionTimeout = globalThis.setTimeout(async () => {
this.clearDelayedUpdateInlineMenuPositionTimeout();
await this.updateInlineMenuPositionAfterSubFrameRebuild(sender);
}, 650);
}
/**
@ -583,9 +582,10 @@ export class OverlayBackground implements OverlayBackgroundInterface {
}
}
private clearUpdateInlineMenuPositionTimeout() {
if (this.updateInlineMenuPositionTimeout) {
clearTimeout(this.updateInlineMenuPositionTimeout);
private clearDelayedUpdateInlineMenuPositionTimeout() {
if (this.delayedUpdateInlineMenuPositionTimeout) {
clearTimeout(this.delayedUpdateInlineMenuPositionTimeout);
this.delayedUpdateInlineMenuPositionTimeout = null;
}
}
@ -628,7 +628,16 @@ export class OverlayBackground implements OverlayBackgroundInterface {
{ overlayElement }: { overlayElement?: string },
sender: chrome.runtime.MessageSender,
) {
if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) {
if (this.delayedUpdateInlineMenuPositionTimeout && this.isFieldCurrentlyFocused) {
this.closeInlineMenu(sender, { forceCloseInlineMenu: true });
return;
}
if (
!overlayElement ||
sender.tab.id !== this.focusedFieldData?.tabId ||
this.delayedUpdateInlineMenuPositionTimeout
) {
return;
}
@ -1159,12 +1168,12 @@ export class OverlayBackground implements OverlayBackgroundInterface {
) => {
const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command];
if (!handler) {
return;
return null;
}
const messageResponse = handler({ message, sender });
if (typeof messageResponse === "undefined") {
return;
return null;
}
Promise.resolve(messageResponse)

View File

@ -178,12 +178,12 @@ class AutofillInit implements AutofillInitInterface {
const command: string = message.command;
const handler: CallableFunction | undefined = this.getExtensionMessageHandler(command);
if (!handler) {
return;
return null;
}
const messageResponse = handler({ message, sender });
if (typeof messageResponse === "undefined") {
return;
return null;
}
void Promise.resolve(messageResponse).then((response) => sendResponse(response));

View File

@ -295,12 +295,12 @@ export class Fido2Background implements Fido2BackgroundInterface {
) => {
const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command];
if (!handler) {
return;
return null;
}
const messageResponse = handler({ message, sender });
if (typeof messageResponse === "undefined") {
return;
return null;
}
Promise.resolve(messageResponse)

View File

@ -29,7 +29,7 @@ export type AutofillOverlayContentExtensionMessageHandlers = {
getSubFrameOffsets: ({ message }: AutofillExtensionMessageParam) => Promise<SubFrameOffsetData>;
getSubFrameOffsetsFromWindowMessage: ({ message }: AutofillExtensionMessageParam) => void;
checkMostRecentlyFocusedFieldHasValue: () => boolean;
setupAutofillInlineMenuReflowObserver: () => void;
setupRebuildSubFrameOffsetsListeners: () => void;
destroyAutofillInlineMenuListeners: () => void;
};

View File

@ -18,12 +18,7 @@ import {
import AutofillField from "../models/autofill-field";
import AutofillPageDetails from "../models/autofill-page-details";
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
import {
elementIsFillableFormField,
getAttributeBoolean,
sendExtensionMessage,
throttle,
} from "../utils";
import { elementIsFillableFormField, getAttributeBoolean, sendExtensionMessage } from "../utils";
import {
AutofillOverlayContentExtensionMessageHandlers,
@ -67,7 +62,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
getSubFrameOffsetsFromWindowMessage: ({ message }) =>
this.getSubFrameOffsetsFromWindowMessage(message),
checkMostRecentlyFocusedFieldHasValue: () => this.mostRecentlyFocusedFieldHasValue(),
setupAutofillInlineMenuReflowObserver: () => this.setupPageReflowEventListeners(),
setupRebuildSubFrameOffsetsListeners: () => this.setupRebuildSubFrameOffsetsListeners(),
destroyAutofillInlineMenuListeners: () => this.destroy(),
};
@ -759,46 +754,248 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
this.inlineMenuVisibility = inlineMenuVisibility || AutofillOverlayVisibility.OnFieldFocus;
}
private setupPageReflowEventListeners() {
if (this.reflowPerformanceObserver || this.reflowMutationObserver) {
return;
}
/**
* Returns a value that indicates if we should hide the inline menu list due to a filled field.
*
* @param formFieldElement - The form field element that triggered the focus event.
*/
private async hideInlineMenuListOnFilledField(
formFieldElement?: FillableFormFieldElement,
): Promise<boolean> {
return (
formFieldElement?.value &&
((await this.isInlineMenuCiphersPopulated()) || !this.isUserAuthed())
);
}
if ("PerformanceObserver" in window && "LayoutShift" in window) {
this.reflowPerformanceObserver = new PerformanceObserver(
throttle(this.updateSubFrameOffsetsFromLayoutShiftEvent.bind(this), 100),
);
this.reflowPerformanceObserver.observe({ type: "layout-shift", buffered: true });
/**
* Indicates whether the most recently focused field has a value.
*/
private mostRecentlyFocusedFieldHasValue() {
return Boolean((this.mostRecentlyFocusedField as FillableFormFieldElement)?.value);
}
return;
}
if (globalThis.window.top !== globalThis.window && this.formFieldElements.size > 0) {
this.setupRebuildSubFrameOffsetsEventListeners();
/**
* Updates the local reference to the inline menu visibility setting.
*
* @param data - The data object from the extension message.
*/
private updateInlineMenuVisibility({ data }: AutofillExtensionMessage) {
if (!isNaN(data?.inlineMenuVisibility)) {
this.inlineMenuVisibility = data.inlineMenuVisibility;
}
}
private updateSubFrameOffsetsFromLayoutShiftEvent = (list: any) => {
const entries: any[] = list.getEntries();
for (let index = 0; index < entries.length; index++) {
const entry = entries[index];
if (entry.sources?.length) {
this.updateSubFrameForReflow();
return;
/**
* Checks if a field is currently filling within an frame in the tab.
*/
private async isFieldCurrentlyFilling() {
return (await this.sendExtensionMessage("checkIsFieldCurrentlyFilling")) === true;
}
/**
* Checks if the inline menu button is visible at the top frame.
*/
private async isInlineMenuButtonVisible() {
return (await this.sendExtensionMessage("checkIsAutofillInlineMenuButtonVisible")) === true;
}
/**
* Checks if the inline menu list if visible at the top frame.
*/
private async isInlineMenuListVisible() {
return (await this.sendExtensionMessage("checkIsAutofillInlineMenuListVisible")) === true;
}
/**
* Checks if the current tab contains ciphers that can be used to populate the inline menu.
*/
private async isInlineMenuCiphersPopulated() {
return (await this.sendExtensionMessage("checkIsInlineMenuCiphersPopulated")) === true;
}
/**
* Triggers a validation to ensure that the inline menu is repositioned only when the
* current frame contains the focused field at any given depth level.
*/
private async checkShouldRepositionInlineMenu() {
return (await this.sendExtensionMessage("checkShouldRepositionInlineMenu")) === true;
}
/**
* Gets the root node of the passed element and returns the active element within that root node.
*
* @param element - The element to get the root node active element for.
*/
private getRootNodeActiveElement(element: Element): Element {
if (!element) {
return null;
}
const documentRoot = element.getRootNode() as ShadowRoot | Document;
return documentRoot?.activeElement;
}
/**
* Queries all iframe elements within the document and returns the
* sub frame offsets for each iframe element.
*
* @param message - The message object from the extension.
*/
private async getSubFrameOffsets(
message: AutofillExtensionMessage,
): Promise<SubFrameOffsetData | null> {
const { subFrameUrl } = message;
const subFrameUrlWithoutTrailingSlash = subFrameUrl?.replace(/\/$/, "");
let iframeElement: HTMLIFrameElement | null = null;
const iframeElements = globalThis.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);
}
/**
* Posts a message to the parent frame to calculate the sub frame offset of the current frame.
*
* @param message - The message object from the extension.
*/
private getSubFrameOffsetsFromWindowMessage(message: any) {
globalThis.parent.postMessage(
{
command: "calculateSubFramePositioning",
subFrameData: {
url: window.location.href,
frameId: message.subFrameId,
left: 0,
top: 0,
parentFrameIds: [],
subFrameDepth: 0,
} as SubFrameDataFromWindowMessage,
},
"*",
);
}
/**
* Calculates the bounding rect for the queried frame and returns the
* offset data for the sub frame.
*
* @param iframeElement - The iframe element to calculate the sub frame offsets for.
* @param subFrameUrl - The URL of the sub frame.
* @param frameId - The frame ID of the sub frame.
*/
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")) || 0;
const paddingTop = parseInt(iframeStyles.getPropertyValue("padding-top")) || 0;
const borderWidthLeft = parseInt(iframeStyles.getPropertyValue("border-left-width")) || 0;
const borderWidthTop = parseInt(iframeStyles.getPropertyValue("border-top-width")) || 0;
return {
url: subFrameUrl,
frameId,
top: iframeRect.top + paddingTop + borderWidthTop,
left: iframeRect.left + paddingLeft + borderWidthLeft,
};
}
/**
* Calculates the sub frame positioning for the current frame
* through all parent frames until the top frame is reached.
*
* @param event - The message event.
*/
private calculateSubFramePositioning = async (event: MessageEvent) => {
const subFrameData: SubFrameDataFromWindowMessage = event.data.subFrameData;
subFrameData.subFrameDepth++;
if (subFrameData.subFrameDepth >= MAX_SUB_FRAME_DEPTH) {
void this.sendExtensionMessage("destroyAutofillInlineMenuListeners", { subFrameData });
return;
}
let subFrameOffsets: SubFrameOffsetData;
const iframes = globalThis.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;
const parentFrameId = await this.sendExtensionMessage("getCurrentTabFrameId");
if (typeof parentFrameId !== "undefined") {
subFrameData.parentFrameIds.push(parentFrameId);
}
break;
}
}
if (globalThis.window.self !== globalThis.window.top) {
globalThis.parent.postMessage({ command: "calculateSubFramePositioning", subFrameData }, "*");
return;
}
void this.sendExtensionMessage("updateSubFrameData", { subFrameData });
};
/**
* Sets up global event listeners and the mutation
* observer to facilitate required changes to the
* 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();
};
/**
* Handles window messages that are sent to the current frame. Will trigger a
* calculation of the sub frame offsets through the parent frame.
*
* @param event - The message event.
*/
private handleWindowMessageEvent = (event: MessageEvent) => {
if (event.data?.command === "calculateSubFramePositioning") {
void this.calculateSubFramePositioning(event);
}
};
private updateSubFrameForReflow = () => {
// console.log("update sub frame reflow");
if (this.userInteractionEventTimeout) {
this.clearUserInteractionEventTimeout();
void this.toggleInlineMenuHidden(false, true);
void this.sendExtensionMessage("closeAutofillInlineMenu", {
forceCloseInlineMenu: true,
});
/**
* Handles the visibility change event. This method will remove the
* autofill overlay if the document is not visible.
*/
private handleVisibilityChangeEvent = () => {
if (!this.mostRecentlyFocusedField || globalThis.document.visibilityState === "visible") {
return;
}
void this.sendExtensionMessage("updateSubFrameOffsetsForReflowEvent");
this.unsetMostRecentlyFocusedField();
void this.sendExtensionMessage("closeAutofillInlineMenu", {
forceCloseInlineMenu: true,
});
};
/**
@ -911,6 +1108,36 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
}
}
private setupRebuildSubFrameOffsetsListeners = () => {
if (globalThis.window.top === globalThis.window || this.formFieldElements.size < 1) {
return;
}
globalThis.addEventListener(EVENTS.FOCUS, this.handleSubFrameFocusInEvent);
globalThis.document.body.addEventListener(EVENTS.MOUSEENTER, this.handleSubFrameFocusInEvent);
};
private handleSubFrameFocusInEvent = () => {
this.updateSubFrameForReflow();
globalThis.removeEventListener(EVENTS.FOCUS, this.handleSubFrameFocusInEvent);
globalThis.document.body.removeEventListener(
EVENTS.MOUSEENTER,
this.handleSubFrameFocusInEvent,
);
globalThis.addEventListener(EVENTS.BLUR, this.setupRebuildSubFrameOffsetsListeners);
globalThis.document.body.addEventListener(
EVENTS.MOUSELEAVE,
this.setupRebuildSubFrameOffsetsListeners,
);
};
private updateSubFrameForReflow = () => {
this.clearUserInteractionEventTimeout();
this.clearRecalculateSubFrameOffsetsTimeout();
void this.sendExtensionMessage("updateSubFrameOffsetsForReflowEvent");
};
/**
* Checks if the focused field is present within the bounds of the viewport.
* If not present, the inline menu will be closed.
@ -924,274 +1151,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
);
}
/**
* Returns a value that indicates if we should hide the inline menu list due to a filled field.
*
* @param formFieldElement - The form field element that triggered the focus event.
*/
private async hideInlineMenuListOnFilledField(
formFieldElement?: FillableFormFieldElement,
): Promise<boolean> {
return (
formFieldElement?.value &&
((await this.isInlineMenuCiphersPopulated()) || !this.isUserAuthed())
);
}
/**
* Indicates whether the most recently focused field has a value.
*/
private mostRecentlyFocusedFieldHasValue() {
return Boolean((this.mostRecentlyFocusedField as FillableFormFieldElement)?.value);
}
/**
* Sets up global event listeners and the mutation
* observer to facilitate required changes to the
* 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();
};
/**
* Handles the visibility change event. This method will remove the
* autofill overlay if the document is not visible.
*/
private handleVisibilityChangeEvent = () => {
if (!this.mostRecentlyFocusedField || globalThis.document.visibilityState === "visible") {
return;
}
this.unsetMostRecentlyFocusedField();
void this.sendExtensionMessage("closeAutofillInlineMenu", {
forceCloseInlineMenu: true,
});
};
/**
* Gets the root node of the passed element and returns the active element within that root node.
*
* @param element - The element to get the root node active element for.
*/
private getRootNodeActiveElement(element: Element): Element {
if (!element) {
return null;
}
const documentRoot = element.getRootNode() as ShadowRoot | Document;
return documentRoot?.activeElement;
}
private setupRebuildSubFrameOffsetsEventListeners = () => {
// console.log("setting up listeners");
globalThis.addEventListener(EVENTS.FOCUS, this.handleSubFrameFocusInEvent);
globalThis.document.body.addEventListener(EVENTS.MOUSEENTER, this.handleSubFrameFocusInEvent);
};
private handleSubFrameFocusInEvent = (event: FocusEvent) => {
// console.log("removing listeners", event);
this.updateSubFrameForReflow();
globalThis.removeEventListener(EVENTS.FOCUS, this.handleSubFrameFocusInEvent);
globalThis.document.body.removeEventListener(
EVENTS.MOUSEENTER,
this.handleSubFrameFocusInEvent,
);
globalThis.addEventListener(EVENTS.BLUR, this.handleSubFrameFocusOutEvent);
globalThis.document.body.addEventListener(EVENTS.MOUSELEAVE, this.handleSubFrameFocusOutEvent);
};
handleSubFrameFocusOutEvent = (event: FocusEvent) => {
// console.log(event);
this.setupRebuildSubFrameOffsetsEventListeners();
};
/**
* Queries all iframe elements within the document and returns the
* sub frame offsets for each iframe element.
*
* @param message - The message object from the extension.
*/
private async getSubFrameOffsets(
message: AutofillExtensionMessage,
): Promise<SubFrameOffsetData | null> {
const { subFrameUrl } = message;
const subFrameUrlWithoutTrailingSlash = subFrameUrl?.replace(/\/$/, "");
let iframeElement: HTMLIFrameElement | null = null;
const iframeElements = globalThis.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);
}
/**
* Calculates the bounding rect for the queried frame and returns the
* offset data for the sub frame.
*
* @param iframeElement - The iframe element to calculate the sub frame offsets for.
* @param subFrameUrl - The URL of the sub frame.
* @param frameId - The frame ID of the sub frame.
*/
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")) || 0;
const paddingTop = parseInt(iframeStyles.getPropertyValue("padding-top")) || 0;
const borderWidthLeft = parseInt(iframeStyles.getPropertyValue("border-left-width")) || 0;
const borderWidthTop = parseInt(iframeStyles.getPropertyValue("border-top-width")) || 0;
return {
url: subFrameUrl,
frameId,
top: iframeRect.top + paddingTop + borderWidthTop,
left: iframeRect.left + paddingLeft + borderWidthLeft,
};
}
/**
* Posts a message to the parent frame to calculate the sub frame offset of the current frame.
*
* @param message - The message object from the extension.
*/
private getSubFrameOffsetsFromWindowMessage(message: any) {
globalThis.parent.postMessage(
{
command: "calculateSubFramePositioning",
subFrameData: {
url: window.location.href,
frameId: message.subFrameId,
left: 0,
top: 0,
parentFrameIds: [],
subFrameDepth: 0,
} as SubFrameDataFromWindowMessage,
},
"*",
);
}
/**
* Handles window messages that are sent to the current frame. Will trigger a
* calculation of the sub frame offsets through the parent frame.
*
* @param event - The message event.
*/
private handleWindowMessageEvent = (event: MessageEvent) => {
if (event.data?.command === "calculateSubFramePositioning") {
void this.calculateSubFramePositioning(event);
}
};
/**
* Calculates the sub frame positioning for the current frame
* through all parent frames until the top frame is reached.
*
* @param event - The message event.
*/
private calculateSubFramePositioning = async (event: MessageEvent) => {
const subFrameData: SubFrameDataFromWindowMessage = event.data.subFrameData;
subFrameData.subFrameDepth++;
if (subFrameData.subFrameDepth >= MAX_SUB_FRAME_DEPTH) {
void this.sendExtensionMessage("destroyAutofillInlineMenuListeners", { subFrameData });
return;
}
let subFrameOffsets: SubFrameOffsetData;
const iframes = globalThis.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;
const parentFrameId = await this.sendExtensionMessage("getCurrentTabFrameId");
if (typeof parentFrameId !== "undefined") {
subFrameData.parentFrameIds.push(parentFrameId);
}
break;
}
}
if (globalThis.window.self !== globalThis.window.top) {
globalThis.parent.postMessage({ command: "calculateSubFramePositioning", subFrameData }, "*");
return;
}
void this.sendExtensionMessage("updateSubFrameData", { subFrameData });
};
/**
* Updates the local reference to the inline menu visibility setting.
*
* @param data - The data object from the extension message.
*/
private updateInlineMenuVisibility({ data }: AutofillExtensionMessage) {
if (!isNaN(data?.inlineMenuVisibility)) {
this.inlineMenuVisibility = data.inlineMenuVisibility;
}
}
/**
* Checks if a field is currently filling within an frame in the tab.
*/
private async isFieldCurrentlyFilling() {
return (await this.sendExtensionMessage("checkIsFieldCurrentlyFilling")) === true;
}
/**
* Checks if the inline menu button is visible at the top frame.
*/
private async isInlineMenuButtonVisible() {
return (await this.sendExtensionMessage("checkIsAutofillInlineMenuButtonVisible")) === true;
}
/**
* Checks if the inline menu list if visible at the top frame.
*/
private async isInlineMenuListVisible() {
return (await this.sendExtensionMessage("checkIsAutofillInlineMenuListVisible")) === true;
}
/**
* Checks if the current tab contains ciphers that can be used to populate the inline menu.
*/
private async isInlineMenuCiphersPopulated() {
return (await this.sendExtensionMessage("checkIsInlineMenuCiphersPopulated")) === true;
}
/**
* Triggers a validation to ensure that the inline menu is repositioned only when the
* current frame contains the focused field at any given depth level.
*/
private async checkShouldRepositionInlineMenu() {
return (await this.sendExtensionMessage("checkShouldRepositionInlineMenu")) === true;
}
/**
* Destroys the autofill overlay content service. This method will
* disconnect the mutation observers and remove all event listeners.

View File

@ -330,14 +330,3 @@ export function getPropertyOrAttribute(element: HTMLElement, attributeName: stri
return element.getAttribute(attributeName);
}
export function throttle(callback: () => void, limit: number) {
let waitingDelay = false;
return function (...args: unknown[]) {
if (!waitingDelay) {
callback.apply(this, args);
waitingDelay = true;
globalThis.setTimeout(() => (waitingDelay = false), limit);
}
};
}