1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 01:33:33 +00:00

[PM-5189] Separating the inline menu UI elements from the base AutofillOverlayContentService and setting up messaging to allow for propagation of those elements

This commit is contained in:
Cesar Gonzalez
2024-03-20 12:36:25 -05:00
parent a3b12581f7
commit 0af95bb2be
13 changed files with 3213 additions and 2716 deletions

View File

@@ -45,7 +45,8 @@ type OverlayBackgroundExtensionMessage = {
sender?: string;
details?: AutofillPageDetails;
overlayElement?: string;
display?: string;
forceCloseOverlay?: boolean;
isOverlayHidden?: boolean;
data?: LockedVaultPendingNotificationsData;
} & OverlayAddNewItemMessage;
@@ -59,6 +60,8 @@ type OverlayPortMessage = {
type FocusedFieldData = {
focusedFieldStyles: Partial<CSSStyleDeclaration>;
focusedFieldRects: Partial<DOMRect>;
tabId?: number;
frameId?: number;
};
type OverlayCipherData = {
@@ -83,13 +86,17 @@ type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSende
type OverlayBackgroundExtensionMessageHandlers = {
[key: string]: CallableFunction;
openAutofillOverlay: () => void;
closeAutofillOverlay: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
autofillOverlayElementClosed: ({ message }: BackgroundMessageParam) => void;
autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
getAutofillOverlayVisibility: () => void;
checkAutofillOverlayFocused: () => void;
focusAutofillOverlayList: () => void;
updateAutofillOverlayPosition: ({ message }: BackgroundMessageParam) => void;
updateAutofillOverlayHidden: ({ message }: BackgroundMessageParam) => void;
updateAutofillOverlayPosition: ({
message,
sender,
}: BackgroundOnMessageHandlerParams) => Promise<void>;
updateAutofillOverlayHidden: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
unlockCompleted: ({ message }: BackgroundMessageParam) => void;

View File

@@ -783,17 +783,24 @@ describe("OverlayBackground", () => {
});
});
it("will post a message to the overlay list facilitating an update of the list's position", () => {
it("will post a message to the overlay list facilitating an update of the list's position", async () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const focusedFieldData = createFocusedFieldDataMock();
sendExtensionRuntimeMessage({ command: "updateFocusedFieldData", focusedFieldData });
overlayBackground["updateOverlayPosition"]({
overlayElement: AutofillOverlayElement.List,
});
sendExtensionRuntimeMessage({
command: "updateAutofillOverlayPosition",
overlayElement: AutofillOverlayElement.List,
});
await overlayBackground["updateOverlayPosition"](
{
overlayElement: AutofillOverlayElement.List,
},
sender,
);
sendExtensionRuntimeMessage(
{
command: "updateAutofillOverlayPosition",
overlayElement: AutofillOverlayElement.List,
},
sender,
);
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateIframePosition",

View File

@@ -56,17 +56,21 @@ class OverlayBackground implements OverlayBackgroundInterface {
private overlayButtonPort: chrome.runtime.Port;
private overlayListPort: chrome.runtime.Port;
private focusedFieldData: FocusedFieldData;
private isFieldCurrentlyFocused: boolean;
private isCurrentlyFilling: boolean;
private overlayPageTranslations: Record<string, string>;
private readonly iconsServerUrl: string;
private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = {
openAutofillOverlay: () => this.openOverlay(false),
closeAutofillOverlay: ({ message, sender }) => this.closeOverlay(sender, message),
autofillOverlayElementClosed: ({ message }) => this.overlayElementClosed(message),
autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender),
getAutofillOverlayVisibility: () => this.getOverlayVisibility(),
checkAutofillOverlayFocused: () => this.checkOverlayFocused(),
focusAutofillOverlayList: () => this.focusOverlayList(),
updateAutofillOverlayPosition: ({ message }) => this.updateOverlayPosition(message),
updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message),
updateAutofillOverlayPosition: ({ message, sender }) =>
this.updateOverlayPosition(message, sender),
updateAutofillOverlayHidden: ({ message, sender }) => this.updateOverlayHidden(message, sender),
updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender),
collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
unlockCompleted: ({ message }) => this.unlockCompleted(message),
@@ -75,14 +79,16 @@ class OverlayBackground implements OverlayBackgroundInterface {
};
private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = {
overlayButtonClicked: ({ port }) => this.handleOverlayButtonClicked(port),
closeAutofillOverlay: ({ port }) => this.closeOverlay(port),
forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true),
closeAutofillOverlay: ({ port }) => this.closeOverlay(port.sender),
forceCloseAutofillOverlay: ({ port }) =>
this.closeOverlay(port.sender, { forceCloseOverlay: true }),
overlayPageBlurred: () => this.checkOverlayListFocused(),
redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port),
};
private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = {
checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(),
forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true),
forceCloseAutofillOverlay: ({ port }) =>
this.closeOverlay(port.sender, { forceCloseOverlay: true }),
overlayPageBlurred: () => this.checkOverlayButtonFocused(),
unlockVault: ({ port }) => this.unlockVault(port),
fillSelectedListItem: ({ message, port }) => this.fillSelectedOverlayListItem(message, port),
@@ -216,7 +222,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
};
if (pageDetails.frameId !== 0 && pageDetails.details.fields.length) {
void this.buildSubFrameOffset(pageDetails);
void this.buildSubFrameOffsets(pageDetails);
}
const pageDetailsMap = this.pageDetailsForTab[sender.tab.id];
@@ -228,7 +234,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
pageDetailsMap.set(sender.frameId, pageDetails);
}
private async buildSubFrameOffset({ tab, frameId, details }: PageDetail) {
private async buildSubFrameOffsets({ tab, frameId, details }: PageDetail) {
const tabId = tab.id;
let subFrameOffsetsForTab = this.subFrameOffsetsForTab[tabId];
if (!subFrameOffsetsForTab) {
@@ -335,12 +341,42 @@ class OverlayBackground implements OverlayBackgroundInterface {
* Sends a message to the sender tab to close the autofill overlay.
*
* @param sender - The sender of the port message
* @param forceCloseOverlay - Identifies whether the overlay should be force closed
* @param forceCloseOverlay - Identifies whether the overlay should be forced closed
* @param overlayElement - The overlay element to close, either the list or button
*/
private closeOverlay({ sender }: chrome.runtime.Port, forceCloseOverlay = false) {
// 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
BrowserApi.tabSendMessageData(sender.tab, "closeAutofillOverlay", { forceCloseOverlay });
private closeOverlay(
sender: chrome.runtime.MessageSender,
{
forceCloseOverlay,
overlayElement,
}: { forceCloseOverlay?: boolean; overlayElement?: string } = {},
) {
if (forceCloseOverlay) {
void BrowserApi.tabSendMessage(sender.tab, { command: "closeInlineMenu" }, { frameId: 0 });
return;
}
if (this.isFieldCurrentlyFocused) {
return;
}
if (this.isCurrentlyFilling) {
void BrowserApi.tabSendMessage(
sender.tab,
{
command: "closeInlineMenu",
overlayElement: AutofillOverlayElement.List,
},
{ frameId: 0 },
);
return;
}
void BrowserApi.tabSendMessage(
sender.tab,
{ command: "closeInlineMenu", overlayElement },
{ frameId: 0 },
);
}
/**
@@ -366,16 +402,32 @@ class OverlayBackground implements OverlayBackgroundInterface {
* is based on the focused field's position and dimensions.
*
* @param overlayElement - The overlay element to update, either the list or button
* @param sender - The sender of the extension message
*/
private updateOverlayPosition({ overlayElement }: { overlayElement?: string }) {
private async updateOverlayPosition(
{ overlayElement }: { overlayElement?: string },
sender: chrome.runtime.MessageSender,
) {
if (!overlayElement) {
return;
}
await BrowserApi.tabSendMessage(
sender.tab,
{ command: "updateInlineMenuElementsPosition" },
{ frameId: 0 },
);
const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id];
let subFrameOffsets: SubFrameOffsetData;
if (subFrameOffsetsForTab) {
subFrameOffsets = subFrameOffsetsForTab.get(sender.frameId);
}
if (overlayElement === AutofillOverlayElement.Button) {
this.overlayButtonPort?.postMessage({
command: "updateIframePosition",
styles: this.getOverlayButtonPosition(),
styles: this.getOverlayButtonPosition(subFrameOffsets),
});
return;
@@ -383,7 +435,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
this.overlayListPort?.postMessage({
command: "updateIframePosition",
styles: this.getOverlayListPosition(),
styles: this.getOverlayListPosition(subFrameOffsets),
});
}
@@ -391,11 +443,14 @@ class OverlayBackground implements OverlayBackgroundInterface {
* Gets the position of the focused field and calculates the position
* of the overlay button based on the focused field's position and dimensions.
*/
private getOverlayButtonPosition() {
private getOverlayButtonPosition(subFrameOffsets: SubFrameOffsetData) {
if (!this.focusedFieldData) {
return;
}
const subFrameTopOffset = subFrameOffsets?.top || 0;
const subFrameLeftOffset = subFrameOffsets?.left || 0;
const { top, left, width, height } = this.focusedFieldData.focusedFieldRects;
const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles;
let elementOffset = height * 0.37;
@@ -403,15 +458,15 @@ class OverlayBackground implements OverlayBackgroundInterface {
elementOffset = height >= 50 ? height * 0.47 : height * 0.42;
}
const elementHeight = height - elementOffset;
const elementTopPosition = top + elementOffset / 2;
let elementLeftPosition = left + width - height + elementOffset / 2;
const fieldPaddingRight = parseInt(paddingRight, 10);
const fieldPaddingLeft = parseInt(paddingLeft, 10);
if (fieldPaddingRight > fieldPaddingLeft) {
elementLeftPosition = left + width - height - (fieldPaddingRight - elementOffset + 2);
}
const elementHeight = height - elementOffset;
const elementTopPosition = subFrameTopOffset + top + elementOffset / 2;
const elementLeftPosition =
fieldPaddingRight > fieldPaddingLeft
? subFrameLeftOffset + left + width - height - (fieldPaddingRight - elementOffset + 2)
: subFrameLeftOffset + left + width - height + elementOffset / 2;
return {
top: `${Math.round(elementTopPosition)}px`,
@@ -425,16 +480,19 @@ class OverlayBackground implements OverlayBackgroundInterface {
* Gets the position of the focused field and calculates the position
* of the overlay list based on the focused field's position and dimensions.
*/
private getOverlayListPosition() {
private getOverlayListPosition(subFrameOffsets: SubFrameOffsetData) {
if (!this.focusedFieldData) {
return;
}
const subFrameTopOffset = subFrameOffsets?.top || 0;
const subFrameLeftOffset = subFrameOffsets?.left || 0;
const { top, left, width, height } = this.focusedFieldData.focusedFieldRects;
return {
width: `${Math.round(width)}px`,
top: `${Math.round(top + height)}px`,
left: `${Math.round(left)}px`,
top: `${Math.round(top + height + subFrameTopOffset)}px`,
left: `${Math.round(left + subFrameLeftOffset)}px`,
};
}
@@ -442,26 +500,34 @@ class OverlayBackground implements OverlayBackgroundInterface {
* Sets the focused field data to the data passed in the extension message.
*
* @param focusedFieldData - Contains the rects and styles of the focused field.
* @param sender - The sender of the extension message
*/
private setFocusedFieldData(
{ focusedFieldData }: OverlayBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
) {
this.focusedFieldData = focusedFieldData;
this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id, frameId: sender.frameId };
}
/**
* Updates the overlay's visibility based on the display property passed in the extension message.
*
* @param display - The display property of the overlay, either "block" or "none"
* @param sender - The sender of the extension message
*/
private updateOverlayHidden({ display }: OverlayBackgroundExtensionMessage) {
if (!display) {
return;
}
private updateOverlayHidden(
{ isOverlayHidden }: OverlayBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
) {
const display = isOverlayHidden ? "none" : "block";
const portMessage = { command: "updateOverlayHidden", styles: { display } };
void BrowserApi.tabSendMessage(
sender.tab,
{ command: "toggleInlineMenuHidden", isInlineMenuHidden: isOverlayHidden },
{ frameId: 0 },
);
this.overlayButtonPort?.postMessage(portMessage);
this.overlayListPort?.postMessage(portMessage);
}
@@ -547,7 +613,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
private async unlockVault(port: chrome.runtime.Port) {
const { sender } = port;
this.closeOverlay(port);
this.closeOverlay(port.sender);
const retryMessage: LockedVaultPendingNotificationsData = {
commandToRetry: { message: { command: "openAutofillOverlay" }, sender },
target: "overlay.background",
@@ -761,11 +827,14 @@ class OverlayBackground implements OverlayBackgroundInterface {
translations: this.getTranslations(),
ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null,
});
this.updateOverlayPosition({
overlayElement: isOverlayListPort
? AutofillOverlayElement.List
: AutofillOverlayElement.Button,
});
void this.updateOverlayPosition(
{
overlayElement: isOverlayListPort
? AutofillOverlayElement.List
: AutofillOverlayElement.Button,
},
port.sender,
);
};
/**

View File

@@ -3,7 +3,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
import { SubFrameOffsetData } from "../../background/abstractions/overlay.background";
import AutofillScript from "../../models/autofill-script";
type AutofillExtensionMessage = {
export type AutofillExtensionMessage = {
command: string;
tab?: chrome.tabs.Tab;
sender?: string;
@@ -12,6 +12,7 @@ type AutofillExtensionMessage = {
subFrameUrl?: string;
pageDetailsUrl?: string;
ciphers?: any;
isInlineMenuHidden?: boolean;
data?: {
authStatus?: AuthenticationStatus;
isFocusingFieldElement?: boolean;
@@ -23,15 +24,14 @@ type AutofillExtensionMessage = {
};
};
type AutofillExtensionMessageParam = { message: AutofillExtensionMessage };
export type AutofillExtensionMessageParam = { message: AutofillExtensionMessage };
type AutofillExtensionMessageHandlers = {
export type AutofillExtensionMessageHandlers = {
[key: string]: CallableFunction;
collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void;
collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void;
fillForm: ({ message }: AutofillExtensionMessageParam) => void;
openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void;
closeAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void;
addNewVaultItemFromOverlay: () => void;
redirectOverlayFocusOut: ({ message }: AutofillExtensionMessageParam) => void;
updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void;
@@ -41,9 +41,7 @@ type AutofillExtensionMessageHandlers = {
getSubFrameOffsets: ({ message }: AutofillExtensionMessageParam) => Promise<SubFrameOffsetData>;
};
interface AutofillInit {
export interface AutofillInit {
init(): void;
destroy(): void;
}
export { AutofillExtensionMessage, AutofillExtensionMessageHandlers, AutofillInit };

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
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";
import CollectAutofillContentService from "../services/collect-autofill-content.service";
import DomElementVisibilityService from "../services/dom-element-visibility.service";
@@ -14,6 +15,7 @@ import {
class AutofillInit implements AutofillInitInterface {
private readonly autofillOverlayContentService: AutofillOverlayContentService | undefined;
private readonly inlineMenuElements: InlineMenuElements | undefined;
private readonly domElementVisibilityService: DomElementVisibilityService;
private readonly collectAutofillContentService: CollectAutofillContentService;
private readonly insertAutofillContentService: InsertAutofillContentService;
@@ -22,7 +24,7 @@ class AutofillInit implements AutofillInitInterface {
collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true),
fillForm: ({ message }) => this.fillForm(message),
openAutofillOverlay: ({ message }) => this.openAutofillOverlay(message),
closeAutofillOverlay: ({ message }) => this.removeAutofillOverlay(message),
// closeAutofillOverlay: ({ message }) => this.removeAutofillOverlay(message),
addNewVaultItemFromOverlay: () => this.addNewVaultItemFromOverlay(),
redirectOverlayFocusOut: ({ message }) => this.redirectOverlayFocusOut(message),
updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message),
@@ -37,9 +39,28 @@ class AutofillInit implements AutofillInitInterface {
* CollectAutofillContentService and InsertAutofillContentService classes.
*
* @param autofillOverlayContentService - The autofill overlay content service, potentially undefined.
* @param inlineMenuElements - The inline menu elements, potentially undefined.
*/
constructor(autofillOverlayContentService?: AutofillOverlayContentService) {
constructor(
autofillOverlayContentService?: AutofillOverlayContentService,
inlineMenuElements?: InlineMenuElements,
) {
this.autofillOverlayContentService = autofillOverlayContentService;
if (this.autofillOverlayContentService) {
this.extensionMessageHandlers = Object.assign(
this.extensionMessageHandlers,
this.autofillOverlayContentService.extensionMessageHandlers,
);
}
this.inlineMenuElements = inlineMenuElements;
if (this.inlineMenuElements) {
this.extensionMessageHandlers = Object.assign(
this.extensionMessageHandlers,
this.inlineMenuElements.extensionMessageHandlers,
);
}
this.domElementVisibilityService = new DomElementVisibilityService();
this.collectAutofillContentService = new CollectAutofillContentService(
this.domElementVisibilityService,
@@ -169,33 +190,7 @@ class AutofillInit implements AutofillInitInterface {
}
this.autofillOverlayContentService.blurMostRecentOverlayField();
this.removeAutofillOverlay();
}
/**
* Removes the autofill overlay if the field is not currently focused.
* If the autofill is currently filling, only the overlay list will be
* removed.
*/
private removeAutofillOverlay(message?: AutofillExtensionMessage) {
if (message?.data?.forceCloseOverlay) {
this.autofillOverlayContentService?.removeAutofillOverlay();
return;
}
if (
!this.autofillOverlayContentService ||
this.autofillOverlayContentService.isFieldCurrentlyFocused
) {
return;
}
if (this.autofillOverlayContentService.isCurrentlyFilling) {
this.autofillOverlayContentService.removeAutofillOverlayList();
return;
}
this.autofillOverlayContentService.removeAutofillOverlay();
void sendExtensionMessage("closeAutofillOverlay");
}
/**
@@ -257,7 +252,6 @@ class AutofillInit implements AutofillInitInterface {
const { subFrameUrl } = message;
const subFrameUrlWithoutTrailingSlash = subFrameUrl?.replace(/\/$/, "");
// query iframe based on src attribute
const iframeElement = document.querySelector(
`iframe[src^="${subFrameUrlWithoutTrailingSlash}"]`,
);

View File

@@ -1,3 +1,4 @@
import { InlineMenuElements } from "../overlay/content/inline-menu-elements";
import AutofillOverlayContentService from "../services/autofill-overlay-content.service";
import { setupAutofillInitDisconnectAction } from "../utils";
@@ -6,7 +7,14 @@ import AutofillInit from "./autofill-init";
(function (windowContext) {
if (!windowContext.bitwardenAutofillInit) {
const autofillOverlayContentService = new AutofillOverlayContentService();
windowContext.bitwardenAutofillInit = new AutofillInit(autofillOverlayContentService);
let inlineMenuElements: InlineMenuElements;
if (globalThis.parent === globalThis.top) {
inlineMenuElements = new InlineMenuElements();
}
windowContext.bitwardenAutofillInit = new AutofillInit(
autofillOverlayContentService,
inlineMenuElements,
);
setupAutofillInitDisconnectAction(windowContext);
windowContext.bitwardenAutofillInit.init();

View File

@@ -0,0 +1,12 @@
import { AutofillExtensionMessageParam } from "../../content/abstractions/autofill-init";
export type InlineMenuExtensionMessageHandlers = {
[key: string]: CallableFunction;
closeInlineMenu: ({ message }: AutofillExtensionMessageParam) => void;
updateInlineMenuElementsPosition: () => Promise<[void, void]>;
toggleInlineMenuHidden: ({ message }: AutofillExtensionMessageParam) => void;
};
export interface InlineMenuElements {
extensionMessageHandlers: InlineMenuExtensionMessageHandlers;
}

View File

@@ -0,0 +1,408 @@
import {
sendExtensionMessage,
generateRandomCustomElementName,
setElementStyles,
} from "../../utils";
import { AutofillOverlayElement } from "../../utils/autofill-overlay.enum";
import {
InlineMenuExtensionMessageHandlers,
InlineMenuElements as InlineMenuElementsInterface,
} from "../abstractions/inline-menu-elements";
import AutofillOverlayButtonIframe from "../iframe-content/autofill-overlay-button-iframe";
import AutofillOverlayListIframe from "../iframe-content/autofill-overlay-list-iframe";
export class InlineMenuElements implements InlineMenuElementsInterface {
private readonly sendExtensionMessage = sendExtensionMessage;
private readonly generateRandomCustomElementName = generateRandomCustomElementName;
private readonly setElementStyles = setElementStyles;
private isFirefoxBrowser =
globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 ||
globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1;
private buttonElement: HTMLElement;
private listElement: HTMLElement;
private isButtonVisible = false;
private isListVisible = false;
private overlayElementsMutationObserver: MutationObserver;
private bodyElementMutationObserver: MutationObserver;
private documentElementMutationObserver: MutationObserver;
private mutationObserverIterations = 0;
private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout;
private readonly customElementDefaultStyles: Partial<CSSStyleDeclaration> = {
all: "initial",
position: "fixed",
display: "block",
zIndex: "2147483647",
};
private readonly _extensionMessageHandlers: InlineMenuExtensionMessageHandlers = {
closeInlineMenu: ({ message }) => this.removeInlineMenu(),
updateInlineMenuElementsPosition: () => this.updateInlineMenuElementsPosition(),
toggleInlineMenuHidden: ({ message }) =>
this.toggleInlineMenuHidden(message.isInlineMenuHidden),
};
constructor() {
this.setupMutationObserver();
}
get extensionMessageHandlers() {
return this._extensionMessageHandlers;
}
/**
* Sends a message that facilitates hiding the overlay elements.
*
* @param isHidden - Indicates if the overlay elements should be hidden.
*/
private toggleInlineMenuHidden(isHidden: boolean) {
this.isButtonVisible = !!this.buttonElement && !isHidden;
this.isListVisible = !!this.listElement && !isHidden;
}
/**
* Removes the autofill overlay from the page. This will initially
* unobserve the body element to ensure the mutation observer no
* longer triggers.
*/
private removeInlineMenu = () => {
this.removeBodyElementObserver();
this.removeInlineMenuButton();
this.removeInlineMenuList();
};
/**
* Removes the overlay button from the DOM if it is currently present. Will
* also remove the overlay reposition event listeners.
*/
private removeInlineMenuButton() {
if (!this.buttonElement) {
return;
}
this.buttonElement.remove();
this.isButtonVisible = false;
void this.sendExtensionMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.Button,
});
}
/**
* Removes the overlay list from the DOM if it is currently present.
*/
private removeInlineMenuList() {
if (!this.listElement) {
return;
}
this.listElement.remove();
this.isListVisible = false;
void this.sendExtensionMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.List,
});
}
/**
* Updates the position of both the overlay button and overlay list.
*/
private async updateInlineMenuElementsPosition() {
return Promise.all([this.updateButtonPosition(), this.updateListPosition()]);
}
/**
* Updates the position of the overlay button.
*/
private async updateButtonPosition(): Promise<void> {
return new Promise((resolve) => {
if (!this.buttonElement) {
this.createButton();
this.updateCustomElementDefaultStyles(this.buttonElement);
}
if (!this.isButtonVisible) {
this.appendOverlayElementToBody(this.buttonElement);
this.isButtonVisible = true;
}
resolve();
});
}
/**
* Updates the position of the overlay list.
*/
private async updateListPosition(): Promise<void> {
return new Promise((resolve) => {
if (!this.listElement) {
this.createList();
this.updateCustomElementDefaultStyles(this.listElement);
}
if (!this.isListVisible) {
this.appendOverlayElementToBody(this.listElement);
this.isListVisible = true;
}
resolve();
});
}
/**
* Appends the overlay element to the body element. This method will also
* observe the body element to ensure that the overlay element is not
* interfered with by any DOM changes.
*
* @param element - The overlay element to append to the body element.
*/
private appendOverlayElementToBody(element: HTMLElement) {
this.observeBodyElement();
globalThis.document.body.appendChild(element);
}
/**
* Creates the autofill overlay button element. Will not attempt
* to create the element if it already exists in the DOM.
*/
private createButton() {
if (this.buttonElement) {
return;
}
if (this.isFirefoxBrowser) {
this.buttonElement = globalThis.document.createElement("div");
new AutofillOverlayButtonIframe(this.buttonElement);
return;
}
const customElementName = this.generateRandomCustomElementName();
globalThis.customElements?.define(
customElementName,
class extends HTMLElement {
constructor() {
super();
new AutofillOverlayButtonIframe(this);
}
},
);
this.buttonElement = globalThis.document.createElement(customElementName);
}
/**
* Creates the autofill overlay list element. Will not attempt
* to create the element if it already exists in the DOM.
*/
private createList() {
if (this.listElement) {
return;
}
if (this.isFirefoxBrowser) {
this.listElement = globalThis.document.createElement("div");
new AutofillOverlayListIframe(this.listElement);
return;
}
const customElementName = this.generateRandomCustomElementName();
globalThis.customElements?.define(
customElementName,
class extends HTMLElement {
constructor() {
super();
new AutofillOverlayListIframe(this);
}
},
);
this.listElement = globalThis.document.createElement(customElementName);
}
/**
* Updates the default styles for the custom element. This method will
* remove any styles that are added to the custom element by other methods.
*
* @param element - The custom element to update the default styles for.
*/
private updateCustomElementDefaultStyles(element: HTMLElement) {
this.unobserveCustomElements();
setElementStyles(element, this.customElementDefaultStyles, true);
this.observeCustomElements();
}
/**
* Sets up mutation observers for the overlay elements, the body element, and the
* document element. The mutation observers are used to remove any styles that are
* added to the overlay elements by the website. They are also used to ensure that
* the overlay elements are always present at the bottom of the body element.
*/
private setupMutationObserver = () => {
this.overlayElementsMutationObserver = new MutationObserver(
this.handleOverlayElementMutationObserverUpdate,
);
this.bodyElementMutationObserver = new MutationObserver(
this.handleBodyElementMutationObserverUpdate,
);
};
/**
* Sets up mutation observers to verify that the overlay
* elements are not modified by the website.
*/
private observeCustomElements() {
if (this.buttonElement) {
this.overlayElementsMutationObserver?.observe(this.buttonElement, {
attributes: true,
});
}
if (this.listElement) {
this.overlayElementsMutationObserver?.observe(this.listElement, { attributes: true });
}
}
/**
* Disconnects the mutation observers that are used to verify that the overlay
* elements are not modified by the website.
*/
private unobserveCustomElements() {
this.overlayElementsMutationObserver?.disconnect();
}
/**
* Sets up a mutation observer for the body element. The mutation observer is used
* to ensure that the overlay elements are always present at the bottom of the body
* element.
*/
private observeBodyElement() {
this.bodyElementMutationObserver?.observe(globalThis.document.body, { childList: true });
}
/**
* Disconnects the mutation observer for the body element.
*/
private removeBodyElementObserver() {
this.bodyElementMutationObserver?.disconnect();
}
/**
* Handles the mutation observer update for the overlay elements. This method will
* remove any attributes or styles that might be added to the overlay elements by
* a separate process within the website where this script is injected.
*
* @param mutationRecord - The mutation record that triggered the update.
*/
private handleOverlayElementMutationObserverUpdate = (mutationRecord: MutationRecord[]) => {
if (this.isTriggeringExcessiveMutationObserverIterations()) {
return;
}
for (let recordIndex = 0; recordIndex < mutationRecord.length; recordIndex++) {
const record = mutationRecord[recordIndex];
if (record.type !== "attributes") {
continue;
}
const element = record.target as HTMLElement;
if (record.attributeName !== "style") {
this.removeModifiedElementAttributes(element);
continue;
}
element.removeAttribute("style");
this.updateCustomElementDefaultStyles(element);
}
};
/**
* Removes all elements from a passed overlay
* element except for the style attribute.
*
* @param element - The element to remove the attributes from.
*/
private removeModifiedElementAttributes(element: HTMLElement) {
const attributes = Array.from(element.attributes);
for (let attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) {
const attribute = attributes[attributeIndex];
if (attribute.name === "style") {
continue;
}
element.removeAttribute(attribute.name);
}
}
/**
* Handles the mutation observer update for the body element. This method will
* ensure that the overlay elements are always present at the bottom of the body
* element.
*/
private handleBodyElementMutationObserverUpdate = () => {
if (
(!this.buttonElement && !this.listElement) ||
this.isTriggeringExcessiveMutationObserverIterations()
) {
return;
}
const lastChild = globalThis.document.body.lastElementChild;
const secondToLastChild = lastChild?.previousElementSibling;
const lastChildIsOverlayList = lastChild === this.listElement;
const lastChildIsOverlayButton = lastChild === this.buttonElement;
const secondToLastChildIsOverlayButton = secondToLastChild === this.buttonElement;
if (
(lastChildIsOverlayList && secondToLastChildIsOverlayButton) ||
(lastChildIsOverlayButton && !this.isListVisible)
) {
return;
}
if (
(lastChildIsOverlayList && !secondToLastChildIsOverlayButton) ||
(lastChildIsOverlayButton && this.isListVisible)
) {
globalThis.document.body.insertBefore(this.buttonElement, this.listElement);
return;
}
globalThis.document.body.insertBefore(lastChild, this.buttonElement);
};
/**
* Identifies if the mutation observer is triggering excessive iterations.
* Will trigger a blur of the most recently focused field and remove the
* autofill overlay if any set mutation observer is triggering
* excessive iterations.
*/
private isTriggeringExcessiveMutationObserverIterations() {
if (this.mutationObserverIterationsResetTimeout) {
clearTimeout(this.mutationObserverIterationsResetTimeout);
}
this.mutationObserverIterations++;
this.mutationObserverIterationsResetTimeout = setTimeout(
() => (this.mutationObserverIterations = 0),
2000,
);
if (this.mutationObserverIterations > 100) {
clearTimeout(this.mutationObserverIterationsResetTimeout);
this.mutationObserverIterations = 0;
void this.sendExtensionMessage("blurMostRecentOverlayField");
this.removeInlineMenu();
return true;
}
return false;
}
destroy() {
this.documentElementMutationObserver?.disconnect();
}
}

View File

@@ -245,7 +245,7 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
}
this.updateElementStyles(this.iframe, position);
setTimeout(() => this.updateElementStyles(this.iframe, { opacity: "1" }), 0);
setTimeout(() => this.updateElementStyles(this.iframe, { opacity: "1" }), 75);
this.announceAriaAlert();
}

View File

@@ -3,32 +3,36 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
import AutofillField from "../../models/autofill-field";
import { ElementWithOpId, FormFieldElement } from "../../types";
type OpenAutofillOverlayOptions = {
export type OpenAutofillOverlayOptions = {
isFocusingFieldElement?: boolean;
isOpeningFullOverlay?: boolean;
authStatus?: AuthenticationStatus;
};
interface AutofillOverlayContentService {
export type AutofillOverlayContentExtensionMessageHandlers = {
[key: string]: CallableFunction;
blurMostRecentOverlayField: () => void;
};
export interface AutofillOverlayContentService {
isFieldCurrentlyFocused: boolean;
isCurrentlyFilling: boolean;
isOverlayCiphersPopulated: boolean;
pageDetailsUpdateRequired: boolean;
autofillOverlayVisibility: number;
extensionMessageHandlers: any;
init(): void;
setupAutofillOverlayListenerOnField(
autofillFieldElement: ElementWithOpId<FormFieldElement>,
autofillFieldData: AutofillField,
): Promise<void>;
openAutofillOverlay(options: OpenAutofillOverlayOptions): void;
removeAutofillOverlay(): void;
removeAutofillOverlayButton(): void;
removeAutofillOverlayList(): void;
// removeAutofillOverlay(): void;
// removeAutofillOverlayButton(): void;
// removeAutofillOverlayList(): void;
addNewVaultItem(): void;
redirectOverlayFocusOut(direction: "previous" | "next"): void;
focusMostRecentOverlayField(): void;
blurMostRecentOverlayField(): void;
destroy(): void;
}
export { OpenAutofillOverlayOptions, AutofillOverlayContentService };

View File

@@ -7,18 +7,19 @@ import { EVENTS, AutofillOverlayVisibility } from "@bitwarden/common/autofill/co
import { FocusedFieldData } from "../background/abstractions/overlay.background";
import AutofillField from "../models/autofill-field";
import AutofillOverlayButtonIframe from "../overlay/iframe-content/autofill-overlay-button-iframe";
import AutofillOverlayListIframe from "../overlay/iframe-content/autofill-overlay-list-iframe";
// import AutofillOverlayButtonIframe from "../overlay/iframe-content/autofill-overlay-button-iframe";
// import AutofillOverlayListIframe from "../overlay/iframe-content/autofill-overlay-list-iframe";
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
import {
elementIsFillableFormField,
generateRandomCustomElementName,
// generateRandomCustomElementName,
sendExtensionMessage,
setElementStyles,
// setElementStyles,
} from "../utils";
import { AutofillOverlayElement, RedirectFocusDirection } from "../utils/autofill-overlay.enum";
import {
AutofillOverlayContentExtensionMessageHandlers,
AutofillOverlayContentService as AutofillOverlayContentServiceInterface,
OpenAutofillOverlayOptions,
} from "./abstractions/autofill-overlay-content.service";
@@ -30,10 +31,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
isOverlayCiphersPopulated = false;
pageDetailsUpdateRequired = false;
autofillOverlayVisibility: number;
private isFirefoxBrowser =
globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 ||
globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1;
private readonly generateRandomCustomElementName = generateRandomCustomElementName;
// private isFirefoxBrowser =
// globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 ||
// globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1;
// private readonly generateRandomCustomElementName = generateRandomCustomElementName;
private readonly findTabs = tabbable;
private readonly sendExtensionMessage = sendExtensionMessage;
private formFieldElements: Set<ElementWithOpId<FormFieldElement>> = new Set([]);
@@ -43,24 +44,27 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
private focusableElements: FocusableElement[] = [];
private isOverlayButtonVisible = false;
private isOverlayListVisible = false;
private overlayButtonElement: HTMLElement;
private overlayListElement: HTMLElement;
// private overlayButtonElement: HTMLElement;
// private overlayListElement: HTMLElement;
private mostRecentlyFocusedField: ElementWithOpId<FormFieldElement>;
private focusedFieldData: FocusedFieldData;
private userInteractionEventTimeout: number | NodeJS.Timeout;
private overlayElementsMutationObserver: MutationObserver;
private bodyElementMutationObserver: MutationObserver;
private documentElementMutationObserver: MutationObserver;
private mutationObserverIterations = 0;
private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout;
// private overlayElementsMutationObserver: MutationObserver;
// private bodyElementMutationObserver: MutationObserver;
// private documentElementMutationObserver: MutationObserver;
// private mutationObserverIterations = 0;
// private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout;
private autofillFieldKeywordsMap: WeakMap<AutofillField, string> = new WeakMap();
private eventHandlersMemo: { [key: string]: EventListener } = {};
private readonly customElementDefaultStyles: Partial<CSSStyleDeclaration> = {
all: "initial",
position: "fixed",
display: "block",
zIndex: "2147483647",
readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = {
blurMostRecentOverlayField: () => this.blurMostRecentOverlayField,
};
// private readonly customElementDefaultStyles: Partial<CSSStyleDeclaration> = {
// all: "initial",
// position: "fixed",
// display: "block",
// zIndex: "2147483647",
// };
/**
* Initializes the autofill overlay content service by setting up the mutation observers.
@@ -123,9 +127,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
}
if (this.pageDetailsUpdateRequired) {
// 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
this.sendExtensionMessage("bgCollectPageDetails", {
void this.sendExtensionMessage("bgCollectPageDetails", {
sender: "autofillOverlayContentService",
});
this.pageDetailsUpdateRequired = false;
@@ -164,52 +166,52 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
this.mostRecentlyFocusedField?.blur();
}
/**
* Removes the autofill overlay from the page. This will initially
* unobserve the body element to ensure the mutation observer no
* longer triggers.
*/
removeAutofillOverlay = () => {
this.removeBodyElementObserver();
this.removeAutofillOverlayButton();
this.removeAutofillOverlayList();
};
// /**
// * Removes the autofill overlay from the page. This will initially
// * unobserve the body element to ensure the mutation observer no
// * longer triggers.
// */
// removeAutofillOverlay = () => {
// this.removeBodyElementObserver();
// this.removeAutofillOverlayButton();
// this.removeAutofillOverlayList();
// };
/**
* Removes the overlay button from the DOM if it is currently present. Will
* also remove the overlay reposition event listeners.
*/
removeAutofillOverlayButton() {
if (!this.overlayButtonElement) {
return;
}
this.overlayButtonElement.remove();
this.isOverlayButtonVisible = false;
// 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
this.sendExtensionMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.Button,
});
this.removeOverlayRepositionEventListeners();
}
/**
* Removes the overlay list from the DOM if it is currently present.
*/
removeAutofillOverlayList() {
if (!this.overlayListElement) {
return;
}
this.overlayListElement.remove();
this.isOverlayListVisible = false;
// 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
this.sendExtensionMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.List,
});
}
// /**
// * Removes the overlay button from the DOM if it is currently present. Will
// * also remove the overlay reposition event listeners.
// */
// removeAutofillOverlayButton() {
// if (!this.overlayButtonElement) {
// return;
// }
//
// this.overlayButtonElement.remove();
// this.isOverlayButtonVisible = false;
// // 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
// this.sendExtensionMessage("autofillOverlayElementClosed", {
// overlayElement: AutofillOverlayElement.Button,
// });
// this.removeOverlayRepositionEventListeners();
// }
//
// /**
// * Removes the overlay list from the DOM if it is currently present.
// */
// removeAutofillOverlayList() {
// if (!this.overlayListElement) {
// return;
// }
//
// this.overlayListElement.remove();
// this.isOverlayListVisible = false;
// // 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
// this.sendExtensionMessage("autofillOverlayElementClosed", {
// overlayElement: AutofillOverlayElement.List,
// });
// }
/**
* Formats any found user filled fields for a login cipher and sends a message
@@ -227,9 +229,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
hostname: globalThis.document.location.hostname,
};
// 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
this.sendExtensionMessage("autofillOverlayAddNewVaultItem", { login });
void this.sendExtensionMessage("autofillOverlayAddNewVaultItem", { login });
}
/**
@@ -246,7 +246,8 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
if (direction === RedirectFocusDirection.Current) {
this.focusMostRecentOverlayField();
setTimeout(this.removeAutofillOverlay, 100);
// setTimeout(this.removeAutofillOverlay, 100);
setTimeout(() => void this.sendExtensionMessage("closeAutofillOverlay"), 100);
return;
}
@@ -340,9 +341,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
*/
private handleFormFieldBlurEvent = () => {
this.isFieldCurrentlyFocused = false;
// 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
this.sendExtensionMessage("checkAutofillOverlayFocused");
void this.sendExtensionMessage("checkAutofillOverlayFocused");
};
/**
@@ -356,7 +355,8 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
private handleFormFieldKeyupEvent = (event: KeyboardEvent) => {
const eventCode = event.code;
if (eventCode === "Escape") {
this.removeAutofillOverlay();
// this.removeAutofillOverlay();
void this.sendExtensionMessage("closeAutofillOverlay");
return;
}
@@ -420,7 +420,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
this.storeModifiedFormElement(formFieldElement);
if (formFieldElement.value && (this.isOverlayCiphersPopulated || !this.isUserAuthed())) {
this.removeAutofillOverlayList();
// this.removeAutofillOverlayList();
void this.sendExtensionMessage("closeAutofillOverlay", {
overlayElement: AutofillOverlayElement.List,
});
return;
}
@@ -508,7 +511,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
this.autofillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick ||
(formElementHasValue && initiallyFocusedField !== this.mostRecentlyFocusedField)
) {
this.removeAutofillOverlayList();
// this.removeAutofillOverlayList();
void this.sendExtensionMessage("closeAutofillOverlay", {
overlayElement: AutofillOverlayElement.List,
});
}
if (!formElementHasValue || (!this.isOverlayCiphersPopulated && this.isUserAuthed())) {
@@ -595,19 +601,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
* Updates the position of the overlay button.
*/
private updateOverlayButtonPosition() {
if (!this.overlayButtonElement) {
this.createAutofillOverlayButton();
this.updateCustomElementDefaultStyles(this.overlayButtonElement);
}
if (!this.isOverlayButtonVisible) {
this.appendOverlayElementToBody(this.overlayButtonElement);
this.isOverlayButtonVisible = true;
this.setOverlayRepositionEventListeners();
}
// 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
this.sendExtensionMessage("updateAutofillOverlayPosition", {
void this.sendExtensionMessage("updateAutofillOverlayPosition", {
overlayElement: AutofillOverlayElement.Button,
});
}
@@ -616,34 +610,22 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
* Updates the position of the overlay list.
*/
private updateOverlayListPosition() {
if (!this.overlayListElement) {
this.createAutofillOverlayList();
this.updateCustomElementDefaultStyles(this.overlayListElement);
}
if (!this.isOverlayListVisible) {
this.appendOverlayElementToBody(this.overlayListElement);
this.isOverlayListVisible = true;
}
// 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
this.sendExtensionMessage("updateAutofillOverlayPosition", {
void this.sendExtensionMessage("updateAutofillOverlayPosition", {
overlayElement: AutofillOverlayElement.List,
});
}
/**
* Appends the overlay element to the body element. This method will also
* observe the body element to ensure that the overlay element is not
* interfered with by any DOM changes.
*
* @param element - The overlay element to append to the body element.
*/
private appendOverlayElementToBody(element: HTMLElement) {
this.observeBodyElement();
globalThis.document.body.appendChild(element);
}
// /**
// * Appends the overlay element to the body element. This method will also
// * observe the body element to ensure that the overlay element is not
// * interfered with by any DOM changes.
// *
// * @param element - The overlay element to append to the body element.
// */
// private appendOverlayElementToBody(element: HTMLElement) {
// this.observeBodyElement();
// globalThis.document.body.appendChild(element);
// }
/**
* Sends a message that facilitates hiding the overlay elements.
@@ -651,11 +633,9 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
* @param isHidden - Indicates if the overlay elements should be hidden.
*/
private toggleOverlayHidden(isHidden: boolean) {
const displayValue = isHidden ? "none" : "block";
void this.sendExtensionMessage("updateAutofillOverlayHidden", { display: displayValue });
this.isOverlayButtonVisible = !!this.overlayButtonElement && !isHidden;
this.isOverlayListVisible = !!this.overlayListElement && !isHidden;
void this.sendExtensionMessage("updateAutofillOverlayHidden", {
isOverlayHidden: isHidden,
});
}
/**
@@ -676,9 +656,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
focusedFieldRects: { width, height, top, left },
};
// 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
this.sendExtensionMessage("updateFocusedFieldData", {
void this.sendExtensionMessage("updateFocusedFieldData", {
focusedFieldData: this.focusedFieldData,
});
}
@@ -762,77 +740,77 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
return !isLoginCipherField;
}
/**
* Creates the autofill overlay button element. Will not attempt
* to create the element if it already exists in the DOM.
*/
private createAutofillOverlayButton() {
if (this.overlayButtonElement) {
return;
}
// /**
// * Creates the autofill overlay button element. Will not attempt
// * to create the element if it already exists in the DOM.
// */
// private createAutofillOverlayButton() {
// if (this.overlayButtonElement) {
// return;
// }
//
// if (this.isFirefoxBrowser) {
// this.overlayButtonElement = globalThis.document.createElement("div");
// new AutofillOverlayButtonIframe(this.overlayButtonElement);
//
// return;
// }
//
// const customElementName = this.generateRandomCustomElementName();
// globalThis.customElements?.define(
// customElementName,
// class extends HTMLElement {
// constructor() {
// super();
// new AutofillOverlayButtonIframe(this);
// }
// },
// );
// this.overlayButtonElement = globalThis.document.createElement(customElementName);
// }
//
// /**
// * Creates the autofill overlay list element. Will not attempt
// * to create the element if it already exists in the DOM.
// */
// private createAutofillOverlayList() {
// if (this.overlayListElement) {
// return;
// }
//
// if (this.isFirefoxBrowser) {
// this.overlayListElement = globalThis.document.createElement("div");
// new AutofillOverlayListIframe(this.overlayListElement);
//
// return;
// }
//
// const customElementName = this.generateRandomCustomElementName();
// globalThis.customElements?.define(
// customElementName,
// class extends HTMLElement {
// constructor() {
// super();
// new AutofillOverlayListIframe(this);
// }
// },
// );
// this.overlayListElement = globalThis.document.createElement(customElementName);
// }
if (this.isFirefoxBrowser) {
this.overlayButtonElement = globalThis.document.createElement("div");
new AutofillOverlayButtonIframe(this.overlayButtonElement);
return;
}
const customElementName = this.generateRandomCustomElementName();
globalThis.customElements?.define(
customElementName,
class extends HTMLElement {
constructor() {
super();
new AutofillOverlayButtonIframe(this);
}
},
);
this.overlayButtonElement = globalThis.document.createElement(customElementName);
}
/**
* Creates the autofill overlay list element. Will not attempt
* to create the element if it already exists in the DOM.
*/
private createAutofillOverlayList() {
if (this.overlayListElement) {
return;
}
if (this.isFirefoxBrowser) {
this.overlayListElement = globalThis.document.createElement("div");
new AutofillOverlayListIframe(this.overlayListElement);
return;
}
const customElementName = this.generateRandomCustomElementName();
globalThis.customElements?.define(
customElementName,
class extends HTMLElement {
constructor() {
super();
new AutofillOverlayListIframe(this);
}
},
);
this.overlayListElement = globalThis.document.createElement(customElementName);
}
/**
* Updates the default styles for the custom element. This method will
* remove any styles that are added to the custom element by other methods.
*
* @param element - The custom element to update the default styles for.
*/
private updateCustomElementDefaultStyles(element: HTMLElement) {
this.unobserveCustomElements();
setElementStyles(element, this.customElementDefaultStyles, true);
this.observeCustomElements();
}
// /**
// * Updates the default styles for the custom element. This method will
// * remove any styles that are added to the custom element by other methods.
// *
// * @param element - The custom element to update the default styles for.
// */
// private updateCustomElementDefaultStyles(element: HTMLElement) {
// this.unobserveCustomElements();
//
// setElementStyles(element, this.customElementDefaultStyles, true);
//
// this.observeCustomElements();
// }
/**
* Queries the background script for the autofill overlay visibility setting.
@@ -890,7 +868,8 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
private triggerOverlayRepositionUpdates = async () => {
if (!this.recentlyFocusedFieldIsCurrentlyFocused()) {
this.toggleOverlayHidden(false);
this.removeAutofillOverlay();
// this.removeAutofillOverlay();
void this.sendExtensionMessage("closeAutofillOverlay");
return;
}
@@ -906,7 +885,8 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
return;
}
this.removeAutofillOverlay();
// this.removeAutofillOverlay();
void this.sendExtensionMessage("closeAutofillOverlay");
};
/**
@@ -927,7 +907,8 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
private setupGlobalEventListeners = () => {
globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent);
globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
this.setupMutationObserver();
this.setOverlayRepositionEventListeners();
// this.setupMutationObserver();
};
/**
@@ -940,178 +921,179 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
}
this.mostRecentlyFocusedField = null;
this.removeAutofillOverlay();
// this.removeAutofillOverlay();
void this.sendExtensionMessage("closeAutofillOverlay");
};
/**
* Sets up mutation observers for the overlay elements, the body element, and the
* document element. The mutation observers are used to remove any styles that are
* added to the overlay elements by the website. They are also used to ensure that
* the overlay elements are always present at the bottom of the body element.
*/
private setupMutationObserver = () => {
this.overlayElementsMutationObserver = new MutationObserver(
this.handleOverlayElementMutationObserverUpdate,
);
// /**
// * Sets up mutation observers for the overlay elements, the body element, and the
// * document element. The mutation observers are used to remove any styles that are
// * added to the overlay elements by the website. They are also used to ensure that
// * the overlay elements are always present at the bottom of the body element.
// */
// private setupMutationObserver = () => {
// this.overlayElementsMutationObserver = new MutationObserver(
// this.handleOverlayElementMutationObserverUpdate,
// );
//
// this.bodyElementMutationObserver = new MutationObserver(
// this.handleBodyElementMutationObserverUpdate,
// );
// };
this.bodyElementMutationObserver = new MutationObserver(
this.handleBodyElementMutationObserverUpdate,
);
};
// /**
// * Sets up mutation observers to verify that the overlay
// * elements are not modified by the website.
// */
// private observeCustomElements() {
// if (this.overlayButtonElement) {
// this.overlayElementsMutationObserver?.observe(this.overlayButtonElement, {
// attributes: true,
// });
// }
//
// if (this.overlayListElement) {
// this.overlayElementsMutationObserver?.observe(this.overlayListElement, { attributes: true });
// }
// }
/**
* Sets up mutation observers to verify that the overlay
* elements are not modified by the website.
*/
private observeCustomElements() {
if (this.overlayButtonElement) {
this.overlayElementsMutationObserver?.observe(this.overlayButtonElement, {
attributes: true,
});
}
// /**
// * Disconnects the mutation observers that are used to verify that the overlay
// * elements are not modified by the website.
// */
// private unobserveCustomElements() {
// this.overlayElementsMutationObserver?.disconnect();
// }
if (this.overlayListElement) {
this.overlayElementsMutationObserver?.observe(this.overlayListElement, { attributes: true });
}
}
// /**
// * Sets up a mutation observer for the body element. The mutation observer is used
// * to ensure that the overlay elements are always present at the bottom of the body
// * element.
// */
// private observeBodyElement() {
// this.bodyElementMutationObserver?.observe(globalThis.document.body, { childList: true });
// }
/**
* Disconnects the mutation observers that are used to verify that the overlay
* elements are not modified by the website.
*/
private unobserveCustomElements() {
this.overlayElementsMutationObserver?.disconnect();
}
// /**
// * Disconnects the mutation observer for the body element.
// */
// private removeBodyElementObserver() {
// this.bodyElementMutationObserver?.disconnect();
// }
/**
* Sets up a mutation observer for the body element. The mutation observer is used
* to ensure that the overlay elements are always present at the bottom of the body
* element.
*/
private observeBodyElement() {
this.bodyElementMutationObserver?.observe(globalThis.document.body, { childList: true });
}
// /**
// * Handles the mutation observer update for the overlay elements. This method will
// * remove any attributes or styles that might be added to the overlay elements by
// * a separate process within the website where this script is injected.
// *
// * @param mutationRecord - The mutation record that triggered the update.
// */
// private handleOverlayElementMutationObserverUpdate = (mutationRecord: MutationRecord[]) => {
// if (this.isTriggeringExcessiveMutationObserverIterations()) {
// return;
// }
//
// for (let recordIndex = 0; recordIndex < mutationRecord.length; recordIndex++) {
// const record = mutationRecord[recordIndex];
// if (record.type !== "attributes") {
// continue;
// }
//
// const element = record.target as HTMLElement;
// if (record.attributeName !== "style") {
// this.removeModifiedElementAttributes(element);
//
// continue;
// }
//
// element.removeAttribute("style");
// this.updateCustomElementDefaultStyles(element);
// }
// };
/**
* Disconnects the mutation observer for the body element.
*/
private removeBodyElementObserver() {
this.bodyElementMutationObserver?.disconnect();
}
// /**
// * Removes all elements from a passed overlay
// * element except for the style attribute.
// *
// * @param element - The element to remove the attributes from.
// */
// private removeModifiedElementAttributes(element: HTMLElement) {
// const attributes = Array.from(element.attributes);
// for (let attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) {
// const attribute = attributes[attributeIndex];
// if (attribute.name === "style") {
// continue;
// }
//
// element.removeAttribute(attribute.name);
// }
// }
/**
* Handles the mutation observer update for the overlay elements. This method will
* remove any attributes or styles that might be added to the overlay elements by
* a separate process within the website where this script is injected.
*
* @param mutationRecord - The mutation record that triggered the update.
*/
private handleOverlayElementMutationObserverUpdate = (mutationRecord: MutationRecord[]) => {
if (this.isTriggeringExcessiveMutationObserverIterations()) {
return;
}
// /**
// * Handles the mutation observer update for the body element. This method will
// * ensure that the overlay elements are always present at the bottom of the body
// * element.
// */
// private handleBodyElementMutationObserverUpdate = () => {
// if (
// (!this.overlayButtonElement && !this.overlayListElement) ||
// this.isTriggeringExcessiveMutationObserverIterations()
// ) {
// return;
// }
//
// const lastChild = globalThis.document.body.lastElementChild;
// const secondToLastChild = lastChild?.previousElementSibling;
// const lastChildIsOverlayList = lastChild === this.overlayListElement;
// const lastChildIsOverlayButton = lastChild === this.overlayButtonElement;
// const secondToLastChildIsOverlayButton = secondToLastChild === this.overlayButtonElement;
//
// if (
// (lastChildIsOverlayList && secondToLastChildIsOverlayButton) ||
// (lastChildIsOverlayButton && !this.isOverlayListVisible)
// ) {
// return;
// }
//
// if (
// (lastChildIsOverlayList && !secondToLastChildIsOverlayButton) ||
// (lastChildIsOverlayButton && this.isOverlayListVisible)
// ) {
// globalThis.document.body.insertBefore(this.overlayButtonElement, this.overlayListElement);
// return;
// }
//
// globalThis.document.body.insertBefore(lastChild, this.overlayButtonElement);
// };
for (let recordIndex = 0; recordIndex < mutationRecord.length; recordIndex++) {
const record = mutationRecord[recordIndex];
if (record.type !== "attributes") {
continue;
}
const element = record.target as HTMLElement;
if (record.attributeName !== "style") {
this.removeModifiedElementAttributes(element);
continue;
}
element.removeAttribute("style");
this.updateCustomElementDefaultStyles(element);
}
};
/**
* Removes all elements from a passed overlay
* element except for the style attribute.
*
* @param element - The element to remove the attributes from.
*/
private removeModifiedElementAttributes(element: HTMLElement) {
const attributes = Array.from(element.attributes);
for (let attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) {
const attribute = attributes[attributeIndex];
if (attribute.name === "style") {
continue;
}
element.removeAttribute(attribute.name);
}
}
/**
* Handles the mutation observer update for the body element. This method will
* ensure that the overlay elements are always present at the bottom of the body
* element.
*/
private handleBodyElementMutationObserverUpdate = () => {
if (
(!this.overlayButtonElement && !this.overlayListElement) ||
this.isTriggeringExcessiveMutationObserverIterations()
) {
return;
}
const lastChild = globalThis.document.body.lastElementChild;
const secondToLastChild = lastChild?.previousElementSibling;
const lastChildIsOverlayList = lastChild === this.overlayListElement;
const lastChildIsOverlayButton = lastChild === this.overlayButtonElement;
const secondToLastChildIsOverlayButton = secondToLastChild === this.overlayButtonElement;
if (
(lastChildIsOverlayList && secondToLastChildIsOverlayButton) ||
(lastChildIsOverlayButton && !this.isOverlayListVisible)
) {
return;
}
if (
(lastChildIsOverlayList && !secondToLastChildIsOverlayButton) ||
(lastChildIsOverlayButton && this.isOverlayListVisible)
) {
globalThis.document.body.insertBefore(this.overlayButtonElement, this.overlayListElement);
return;
}
globalThis.document.body.insertBefore(lastChild, this.overlayButtonElement);
};
/**
* Identifies if the mutation observer is triggering excessive iterations.
* Will trigger a blur of the most recently focused field and remove the
* autofill overlay if any set mutation observer is triggering
* excessive iterations.
*/
private isTriggeringExcessiveMutationObserverIterations() {
if (this.mutationObserverIterationsResetTimeout) {
clearTimeout(this.mutationObserverIterationsResetTimeout);
}
this.mutationObserverIterations++;
this.mutationObserverIterationsResetTimeout = setTimeout(
() => (this.mutationObserverIterations = 0),
2000,
);
if (this.mutationObserverIterations > 100) {
clearTimeout(this.mutationObserverIterationsResetTimeout);
this.mutationObserverIterations = 0;
this.blurMostRecentOverlayField();
this.removeAutofillOverlay();
return true;
}
return false;
}
// /**
// * Identifies if the mutation observer is triggering excessive iterations.
// * Will trigger a blur of the most recently focused field and remove the
// * autofill overlay if any set mutation observer is triggering
// * excessive iterations.
// */
// private isTriggeringExcessiveMutationObserverIterations() {
// if (this.mutationObserverIterationsResetTimeout) {
// clearTimeout(this.mutationObserverIterationsResetTimeout);
// }
//
// this.mutationObserverIterations++;
// this.mutationObserverIterationsResetTimeout = setTimeout(
// () => (this.mutationObserverIterations = 0),
// 2000,
// );
//
// if (this.mutationObserverIterations > 100) {
// clearTimeout(this.mutationObserverIterationsResetTimeout);
// this.mutationObserverIterations = 0;
// this.blurMostRecentOverlayField();
// this.removeAutofillOverlay();
//
// return true;
// }
//
// return false;
// }
/**
* Gets the root node of the passed element and returns the active element within that root node.
@@ -1132,7 +1114,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
* disconnect the mutation observers and remove all event listeners.
*/
destroy() {
this.documentElementMutationObserver?.disconnect();
// this.documentElementMutationObserver?.disconnect();
this.clearUserInteractionEventTimeout();
this.formFieldElements.forEach((formFieldElement) => {
this.removeCachedFormFieldEventListeners(formFieldElement);
@@ -1145,7 +1127,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
this.handleVisibilityChangeEvent,
);
globalThis.removeEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
this.removeAutofillOverlay();
// this.removeAutofillOverlay();
this.removeOverlayRepositionEventListeners();
}
}