1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00

[PM-27797] Prevent host page manipulation of inline menu popover attribute (#17400)

* turn off inline experience if host page aggressively competes for top of top-layer

* add alert message for top-layer hijack scenarios

* widen the backoff threshold

* refactor backoff logic to include popover attribute mutations

* improve getPageIsOpaque check

* do not attempt inline menu insertion if it has been disabled for security concerns

* fix typo

* cleanup

* add tests
This commit is contained in:
Jonathan Prusik
2025-11-19 19:14:05 -05:00
committed by GitHub
parent d86c918e71
commit 7c4db701b9
3 changed files with 611 additions and 20 deletions

View File

@@ -2436,6 +2436,9 @@
}
}
},
"topLayerHijackWarning": {
"message": "This page is interfering with the Bitwarden experience. The Bitwarden inline menu has been temporarily disabled as a safety measure."
},
"setMasterPassword": {
"message": "Set master password"
},

View File

@@ -53,13 +53,35 @@ describe("AutofillInlineMenuContentService", () => {
});
});
describe("messageHandlers", () => {
it("returns the extension message handlers", () => {
const handlers = autofillInlineMenuContentService.messageHandlers;
expect(handlers).toHaveProperty("closeAutofillInlineMenu");
expect(handlers).toHaveProperty("appendAutofillInlineMenuToDom");
});
});
describe("isElementInlineMenu", () => {
it("returns true if the passed element is the inline menu", () => {
it("returns true if the passed element is the inline menu list", () => {
const element = document.createElement("div");
autofillInlineMenuContentService["listElement"] = element;
expect(autofillInlineMenuContentService.isElementInlineMenu(element)).toBe(true);
});
it("returns true if the passed element is the inline menu button", () => {
const element = document.createElement("div");
autofillInlineMenuContentService["buttonElement"] = element;
expect(autofillInlineMenuContentService.isElementInlineMenu(element)).toBe(true);
});
it("returns false if the passed element is not the inline menu", () => {
const element = document.createElement("div");
expect(autofillInlineMenuContentService.isElementInlineMenu(element)).toBe(false);
});
});
describe("extension message handlers", () => {
@@ -388,7 +410,7 @@ describe("AutofillInlineMenuContentService", () => {
});
it("closes the inline menu if the page body is not sufficiently opaque", async () => {
document.querySelector("html").style.opacity = "0.9";
document.documentElement.style.opacity = "0.9";
document.body.style.opacity = "0";
await autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]);
@@ -397,7 +419,7 @@ describe("AutofillInlineMenuContentService", () => {
});
it("closes the inline menu if the page html is not sufficiently opaque", async () => {
document.querySelector("html").style.opacity = "0.3";
document.documentElement.style.opacity = "0.3";
document.body.style.opacity = "0.7";
await autofillInlineMenuContentService["handlePageMutations"]([mockHTMLMutationRecord]);
@@ -406,7 +428,7 @@ describe("AutofillInlineMenuContentService", () => {
});
it("does not close the inline menu if the page html and body is sufficiently opaque", async () => {
document.querySelector("html").style.opacity = "0.9";
document.documentElement.style.opacity = "0.9";
document.body.style.opacity = "1";
await autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]);
await waitForIdleCallback();
@@ -599,5 +621,465 @@ describe("AutofillInlineMenuContentService", () => {
overlayElement: AutofillOverlayElement.List,
});
});
it("clears the persistent last child override timeout", () => {
jest.useFakeTimers();
const clearTimeoutSpy = jest.spyOn(globalThis, "clearTimeout");
autofillInlineMenuContentService["handlePersistentLastChildOverrideTimeout"] = setTimeout(
jest.fn(),
500,
);
autofillInlineMenuContentService.destroy();
expect(clearTimeoutSpy).toHaveBeenCalled();
});
it("unobserves page attributes", () => {
const disconnectSpy = jest.spyOn(
autofillInlineMenuContentService["htmlMutationObserver"],
"disconnect",
);
autofillInlineMenuContentService.destroy();
expect(disconnectSpy).toHaveBeenCalled();
});
});
describe("getOwnedTagNames", () => {
it("returns an empty array when no elements are created", () => {
expect(autofillInlineMenuContentService.getOwnedTagNames()).toEqual([]);
});
it("returns the button element tag name", () => {
const buttonElement = document.createElement("div");
autofillInlineMenuContentService["buttonElement"] = buttonElement;
const tagNames = autofillInlineMenuContentService.getOwnedTagNames();
expect(tagNames).toContain("DIV");
});
it("returns both button and list element tag names", () => {
const buttonElement = document.createElement("div");
const listElement = document.createElement("span");
autofillInlineMenuContentService["buttonElement"] = buttonElement;
autofillInlineMenuContentService["listElement"] = listElement;
const tagNames = autofillInlineMenuContentService.getOwnedTagNames();
expect(tagNames).toEqual(["DIV", "SPAN"]);
});
});
describe("getUnownedTopLayerItems", () => {
beforeEach(() => {
document.body.innerHTML = "";
});
it("returns the tag names from button and list elements", () => {
const buttonElement = document.createElement("div");
buttonElement.setAttribute("popover", "manual");
autofillInlineMenuContentService["buttonElement"] = buttonElement;
const listElement = document.createElement("span");
listElement.setAttribute("popover", "manual");
autofillInlineMenuContentService["listElement"] = listElement;
/** Mock querySelectorAll to avoid :modal selector issues in jsdom */
const querySelectorAllSpy = jest
.spyOn(globalThis.document, "querySelectorAll")
.mockReturnValue([] as any);
const items = autofillInlineMenuContentService.getUnownedTopLayerItems();
expect(querySelectorAllSpy).toHaveBeenCalled();
expect(items.length).toBe(0);
});
it("calls querySelectorAll with correct selector when includeCandidates is false", () => {
/** Mock querySelectorAll to avoid :modal selector issues in jsdom */
const querySelectorAllSpy = jest
.spyOn(globalThis.document, "querySelectorAll")
.mockReturnValue([] as any);
autofillInlineMenuContentService.getUnownedTopLayerItems(false);
const calledSelector = querySelectorAllSpy.mock.calls[0][0];
expect(calledSelector).toContain(":modal");
expect(calledSelector).toContain(":popover-open");
});
it("includes candidates selector when requested", () => {
/** Mock querySelectorAll to avoid :modal selector issues in jsdom */
const querySelectorAllSpy = jest
.spyOn(globalThis.document, "querySelectorAll")
.mockReturnValue([] as any);
autofillInlineMenuContentService.getUnownedTopLayerItems(true);
const calledSelector = querySelectorAllSpy.mock.calls[0][0];
expect(calledSelector).toContain("[popover], dialog");
});
});
describe("refreshTopLayerPosition", () => {
it("does nothing when inline menu is disabled", () => {
const getUnownedTopLayerItemsSpy = jest.spyOn(
autofillInlineMenuContentService,
"getUnownedTopLayerItems",
);
autofillInlineMenuContentService["inlineMenuEnabled"] = false;
const buttonElement = document.createElement("div");
autofillInlineMenuContentService["buttonElement"] = buttonElement;
autofillInlineMenuContentService.refreshTopLayerPosition();
// Should exit early and not call `getUnownedTopLayerItems`
expect(getUnownedTopLayerItemsSpy).not.toHaveBeenCalled();
});
it("does nothing when no other top layer items exist", () => {
const buttonElement = document.createElement("div");
autofillInlineMenuContentService["buttonElement"] = buttonElement;
jest
.spyOn(autofillInlineMenuContentService, "getUnownedTopLayerItems")
.mockReturnValue([] as any);
const getElementsByTagSpy = jest.spyOn(globalThis.document, "getElementsByTagName");
autofillInlineMenuContentService.refreshTopLayerPosition();
// Should exit early and not get inline elements to refresh
expect(getElementsByTagSpy).not.toHaveBeenCalled();
});
it("refreshes button popover when button is in document", () => {
jest
.spyOn(autofillInlineMenuContentService, "getUnownedTopLayerItems")
.mockReturnValue([document.createElement("div")] as any);
const buttonElement = document.createElement("div");
buttonElement.setAttribute("popover", "manual");
buttonElement.showPopover = jest.fn();
buttonElement.hidePopover = jest.fn();
document.body.appendChild(buttonElement);
autofillInlineMenuContentService["buttonElement"] = buttonElement;
autofillInlineMenuContentService.refreshTopLayerPosition();
expect(buttonElement.hidePopover).toHaveBeenCalled();
expect(buttonElement.showPopover).toHaveBeenCalled();
});
it("refreshes list popover when list is in document", () => {
jest
.spyOn(autofillInlineMenuContentService, "getUnownedTopLayerItems")
.mockReturnValue([document.createElement("div")] as any);
const listElement = document.createElement("div");
listElement.setAttribute("popover", "manual");
listElement.showPopover = jest.fn();
listElement.hidePopover = jest.fn();
document.body.appendChild(listElement);
autofillInlineMenuContentService["listElement"] = listElement;
autofillInlineMenuContentService.refreshTopLayerPosition();
expect(listElement.hidePopover).toHaveBeenCalled();
expect(listElement.showPopover).toHaveBeenCalled();
});
});
describe("checkAndUpdateRefreshCount", () => {
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date("2023-01-01T00:00:00.000Z"));
});
afterEach(() => {
jest.useRealTimers();
});
it("does nothing when inline menu is disabled", () => {
autofillInlineMenuContentService["inlineMenuEnabled"] = false;
autofillInlineMenuContentService["checkAndUpdateRefreshCount"]("topLayer");
expect(autofillInlineMenuContentService["refreshCountWithinTimeThreshold"].topLayer).toBe(0);
});
it("increments refresh count when within time threshold", () => {
autofillInlineMenuContentService["lastTrackedTimestamp"].topLayer = Date.now() - 1000;
autofillInlineMenuContentService["checkAndUpdateRefreshCount"]("topLayer");
expect(autofillInlineMenuContentService["refreshCountWithinTimeThreshold"].topLayer).toBe(1);
});
it("resets count when outside time threshold", () => {
autofillInlineMenuContentService["lastTrackedTimestamp"].topLayer = Date.now() - 6000;
autofillInlineMenuContentService["refreshCountWithinTimeThreshold"].topLayer = 5;
autofillInlineMenuContentService["checkAndUpdateRefreshCount"]("topLayer");
expect(autofillInlineMenuContentService["refreshCountWithinTimeThreshold"].topLayer).toBe(0);
});
it("disables inline menu and shows alert when count exceeds threshold", () => {
const alertSpy = jest.spyOn(globalThis.window, "alert").mockImplementation();
const checkPageRisksSpy = jest.spyOn(
autofillInlineMenuContentService as any,
"checkPageRisks",
);
autofillInlineMenuContentService["lastTrackedTimestamp"].topLayer = Date.now() - 1000;
autofillInlineMenuContentService["refreshCountWithinTimeThreshold"].topLayer = 6;
autofillInlineMenuContentService["checkAndUpdateRefreshCount"]("topLayer");
expect(autofillInlineMenuContentService["inlineMenuEnabled"]).toBe(false);
expect(alertSpy).toHaveBeenCalled();
expect(checkPageRisksSpy).toHaveBeenCalled();
});
});
describe("refreshPopoverAttribute", () => {
it("calls checkAndUpdateRefreshCount with popoverAttribute type", () => {
const checkSpy = jest.spyOn(
autofillInlineMenuContentService as any,
"checkAndUpdateRefreshCount",
);
const element = document.createElement("div");
element.setAttribute("popover", "auto");
element.showPopover = jest.fn();
autofillInlineMenuContentService["refreshPopoverAttribute"](element);
expect(checkSpy).toHaveBeenCalledWith("popoverAttribute");
expect(element.getAttribute("popover")).toBe("manual");
expect(element.showPopover).toHaveBeenCalled();
});
});
describe("handleInlineMenuElementMutationObserverUpdate - popover attribute", () => {
it("refreshes popover attribute when changed from manual", () => {
const element = document.createElement("div");
element.setAttribute("popover", "auto");
element.showPopover = jest.fn();
const refreshSpy = jest.spyOn(
autofillInlineMenuContentService as any,
"refreshPopoverAttribute",
);
autofillInlineMenuContentService["buttonElement"] = element;
const mockMutation = createMutationRecordMock({
target: element,
type: "attributes",
attributeName: "popover",
});
autofillInlineMenuContentService["handleInlineMenuElementMutationObserverUpdate"]([
mockMutation,
]);
expect(refreshSpy).toHaveBeenCalledWith(element);
});
it("does not refresh popover attribute when already manual", () => {
const element = document.createElement("div");
element.setAttribute("popover", "manual");
const refreshSpy = jest.spyOn(
autofillInlineMenuContentService as any,
"refreshPopoverAttribute",
);
autofillInlineMenuContentService["buttonElement"] = element;
const mockMutation = createMutationRecordMock({
target: element,
type: "attributes",
attributeName: "popover",
});
autofillInlineMenuContentService["handleInlineMenuElementMutationObserverUpdate"]([
mockMutation,
]);
expect(refreshSpy).not.toHaveBeenCalled();
});
});
describe("appendInlineMenuElements when disabled", () => {
beforeEach(() => {
observeContainerMutationsSpy.mockImplementation();
});
it("does not append button when inline menu is disabled", async () => {
autofillInlineMenuContentService["inlineMenuEnabled"] = false;
jest.spyOn(globalThis.document.body, "appendChild");
sendMockExtensionMessage({
command: "appendAutofillInlineMenuToDom",
overlayElement: AutofillOverlayElement.Button,
});
await flushPromises();
expect(globalThis.document.body.appendChild).not.toHaveBeenCalled();
});
it("does not append list when inline menu is disabled", async () => {
autofillInlineMenuContentService["inlineMenuEnabled"] = false;
jest.spyOn(globalThis.document.body, "appendChild");
sendMockExtensionMessage({
command: "appendAutofillInlineMenuToDom",
overlayElement: AutofillOverlayElement.List,
});
await flushPromises();
expect(globalThis.document.body.appendChild).not.toHaveBeenCalled();
});
});
describe("custom element creation for non-Firefox browsers", () => {
beforeEach(() => {
autofillInlineMenuContentService["isFirefoxBrowser"] = false;
observeContainerMutationsSpy.mockImplementation();
});
it("creates a custom element for button in non-Firefox browsers", () => {
const definespy = jest.spyOn(globalThis.customElements, "define");
sendMockExtensionMessage({
command: "appendAutofillInlineMenuToDom",
overlayElement: AutofillOverlayElement.Button,
});
expect(definespy).toHaveBeenCalled();
expect(autofillInlineMenuContentService["buttonElement"]).toBeDefined();
expect(autofillInlineMenuContentService["buttonElement"]?.tagName).not.toBe("DIV");
});
it("creates a custom element for list in non-Firefox browsers", () => {
const defineSpy = jest.spyOn(globalThis.customElements, "define");
sendMockExtensionMessage({
command: "appendAutofillInlineMenuToDom",
overlayElement: AutofillOverlayElement.List,
});
expect(defineSpy).toHaveBeenCalled();
expect(autofillInlineMenuContentService["listElement"]).toBeDefined();
expect(autofillInlineMenuContentService["listElement"]?.tagName).not.toBe("DIV");
});
});
describe("getPageIsOpaque", () => {
it("returns false when no page elements exist", () => {
jest.spyOn(globalThis.document, "querySelectorAll").mockReturnValue([] as any);
const result = autofillInlineMenuContentService["getPageIsOpaque"]();
expect(result).toBe(false);
});
it("returns true when all html and body nodes have sufficient opacity", () => {
jest
.spyOn(globalThis.document, "querySelectorAll")
.mockReturnValue([document.documentElement, document.body] as any);
jest
.spyOn(globalThis.window, "getComputedStyle")
.mockImplementation(() => ({ opacity: "1" }) as CSSStyleDeclaration);
const result = autofillInlineMenuContentService["getPageIsOpaque"]();
expect(result).toBe(true);
});
it("returns false when html opacity is below threshold", () => {
jest
.spyOn(globalThis.document, "querySelectorAll")
.mockReturnValue([document.documentElement, document.body] as any);
let callCount = 0;
jest.spyOn(globalThis.window, "getComputedStyle").mockImplementation(() => {
callCount++;
return { opacity: callCount === 1 ? "0.5" : "1" } as CSSStyleDeclaration;
});
const result = autofillInlineMenuContentService["getPageIsOpaque"]();
expect(result).toBe(false);
});
it("returns false when body opacity is below threshold", () => {
jest
.spyOn(globalThis.document, "querySelectorAll")
.mockReturnValue([document.documentElement, document.body] as any);
let callCount = 0;
jest.spyOn(globalThis.window, "getComputedStyle").mockImplementation(() => {
callCount++;
return { opacity: callCount === 1 ? "1" : "0.5" } as CSSStyleDeclaration;
});
const result = autofillInlineMenuContentService["getPageIsOpaque"]();
expect(result).toBe(false);
});
it("returns false when opacity of at least one duplicate body is below threshold", () => {
const duplicateBody = document.createElement("body");
jest
.spyOn(globalThis.document, "querySelectorAll")
.mockReturnValue([document.documentElement, document.body, duplicateBody] as any);
let callCount = 0;
jest.spyOn(globalThis.window, "getComputedStyle").mockImplementation(() => {
callCount++;
let opacityValue = "0.5";
switch (callCount) {
case 1:
opacityValue = "1";
break;
case 2:
opacityValue = "0.7";
break;
default:
break;
}
return { opacity: opacityValue } as CSSStyleDeclaration;
});
const result = autofillInlineMenuContentService["getPageIsOpaque"]();
expect(result).toBe(false);
});
it("returns true when opacity is above threshold", () => {
jest
.spyOn(globalThis.document, "querySelectorAll")
.mockReturnValue([document.documentElement, document.body] as any);
jest
.spyOn(globalThis.window, "getComputedStyle")
.mockImplementation(() => ({ opacity: "0.7" }) as CSSStyleDeclaration);
const result = autofillInlineMenuContentService["getPageIsOpaque"]();
expect(result).toBe(true);
});
it("returns false when opacity is at threshold", () => {
jest
.spyOn(globalThis.document, "querySelectorAll")
.mockReturnValue([document.documentElement, document.body] as any);
jest
.spyOn(globalThis.window, "getComputedStyle")
.mockImplementation(() => ({ opacity: "0.6" }) as CSSStyleDeclaration);
const result = autofillInlineMenuContentService["getPageIsOpaque"]();
expect(result).toBe(false);
});
});
});

View File

@@ -22,6 +22,19 @@ import {
import { AutofillInlineMenuButtonIframe } from "../iframe-content/autofill-inline-menu-button-iframe";
import { AutofillInlineMenuListIframe } from "../iframe-content/autofill-inline-menu-list-iframe";
const experienceValidationBackoffThresholds = {
topLayer: {
countLimit: 5,
timeSpanLimit: 5000,
},
popoverAttribute: {
countLimit: 10,
timeSpanLimit: 5000,
},
};
type BackoffCheckType = keyof typeof experienceValidationBackoffThresholds;
export class AutofillInlineMenuContentService implements AutofillInlineMenuContentServiceInterface {
private readonly sendExtensionMessage = sendExtensionMessage;
private readonly generateRandomCustomElementName = generateRandomCustomElementName;
@@ -35,6 +48,19 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
private bodyMutationObserver: MutationObserver;
private inlineMenuElementsMutationObserver: MutationObserver;
private containerElementMutationObserver: MutationObserver;
private refreshCountWithinTimeThreshold: { [key in BackoffCheckType]: number } = {
topLayer: 0,
popoverAttribute: 0,
};
private lastTrackedTimestamp = {
topLayer: Date.now(),
popoverAttribute: Date.now(),
};
/**
* Distinct from preventing inline menu script injection, this is for cases
* where the page is subsequently determined to be risky.
*/
private inlineMenuEnabled = true;
private mutationObserverIterations = 0;
private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout;
private handlePersistentLastChildOverrideTimeout: number | NodeJS.Timeout;
@@ -140,6 +166,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
* Updates the position of both the inline menu button and inline menu list.
*/
private async appendInlineMenuElements({ overlayElement }: AutofillExtensionMessage) {
if (!this.inlineMenuEnabled) {
return;
}
if (overlayElement === AutofillOverlayElement.Button) {
return this.appendButtonElement();
}
@@ -151,6 +181,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
* Updates the position of the inline menu button.
*/
private async appendButtonElement(): Promise<void> {
if (!this.inlineMenuEnabled) {
return;
}
if (!this.buttonElement) {
this.createButtonElement();
this.updateCustomElementDefaultStyles(this.buttonElement);
@@ -167,6 +201,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
* Updates the position of the inline menu list.
*/
private async appendListElement(): Promise<void> {
if (!this.inlineMenuEnabled) {
return;
}
if (!this.listElement) {
this.createListElement();
this.updateCustomElementDefaultStyles(this.listElement);
@@ -219,6 +257,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
* to create the element if it already exists in the DOM.
*/
private createButtonElement() {
if (!this.inlineMenuEnabled) {
return;
}
if (this.isFirefoxBrowser) {
this.buttonElement = globalThis.document.createElement("div");
this.buttonElement.setAttribute("popover", "manual");
@@ -247,6 +289,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
* to create the element if it already exists in the DOM.
*/
private createListElement() {
if (!this.inlineMenuEnabled) {
return;
}
if (this.isFirefoxBrowser) {
this.listElement = globalThis.document.createElement("div");
this.listElement.setAttribute("popover", "manual");
@@ -381,14 +427,23 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
}
const element = record.target as HTMLElement;
if (record.attributeName !== "style") {
this.removeModifiedElementAttributes(element);
if (record.attributeName === "popover" && this.inlineMenuEnabled) {
const attributeValue = element.getAttribute(record.attributeName);
if (attributeValue !== "manual") {
this.refreshPopoverAttribute(element);
}
continue;
}
element.removeAttribute("style");
this.updateCustomElementDefaultStyles(element);
if (record.attributeName === "style") {
element.removeAttribute("style");
this.updateCustomElementDefaultStyles(element);
continue;
}
this.removeModifiedElementAttributes(element);
}
};
@@ -402,7 +457,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
const attributes = Array.from(element.attributes);
for (let attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) {
const attribute = attributes[attributeIndex];
if (attribute.name === "style") {
if (attribute.name === "style" || attribute.name === "popover") {
continue;
}
@@ -432,7 +487,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
private checkPageRisks = async () => {
const pageIsOpaque = await this.getPageIsOpaque();
const risksFound = !pageIsOpaque;
const risksFound = !pageIsOpaque || !this.inlineMenuEnabled;
if (risksFound) {
this.closeInlineMenu();
@@ -483,7 +538,49 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
return otherTopLayeritems;
};
/**
* Internally track owned injected experience refreshes as a side-effect
* of host page interference.
*/
private checkAndUpdateRefreshCount = (countType: BackoffCheckType) => {
if (!this.inlineMenuEnabled) {
return;
}
const { countLimit, timeSpanLimit } = experienceValidationBackoffThresholds[countType];
const now = Date.now();
const timeSinceLastTrackedRefresh = now - this.lastTrackedTimestamp[countType];
const currentlyWithinTimeThreshold = timeSinceLastTrackedRefresh <= timeSpanLimit;
const withinCountThreshold = this.refreshCountWithinTimeThreshold[countType] <= countLimit;
if (currentlyWithinTimeThreshold) {
if (withinCountThreshold) {
this.refreshCountWithinTimeThreshold[countType]++;
} else {
// Set inline menu to be off; page is aggressively trying to take top position of top layer
this.inlineMenuEnabled = false;
void this.checkPageRisks();
const warningMessage = chrome.i18n.getMessage("topLayerHijackWarning");
globalThis.window.alert(warningMessage);
}
} else {
this.lastTrackedTimestamp[countType] = now;
this.refreshCountWithinTimeThreshold[countType] = 0;
}
};
private refreshPopoverAttribute = (element: HTMLElement) => {
this.checkAndUpdateRefreshCount("popoverAttribute");
element.setAttribute("popover", "manual");
element.showPopover();
};
refreshTopLayerPosition = () => {
if (!this.inlineMenuEnabled) {
return;
}
const otherTopLayerItems = this.getUnownedTopLayerItems();
// No need to refresh if there are no other top-layer items
@@ -497,6 +594,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
const listInDocument =
this.listElement &&
(globalThis.document.getElementsByTagName(this.listElement.tagName)[0] as HTMLElement);
if (buttonInDocument) {
buttonInDocument.hidePopover();
buttonInDocument.showPopover();
@@ -506,6 +604,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
listInDocument.hidePopover();
listInDocument.showPopover();
}
if (buttonInDocument || listInDocument) {
this.checkAndUpdateRefreshCount("topLayer");
}
};
/**
@@ -515,24 +617,28 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
* `body` (enforced elsewhere).
*/
private getPageIsOpaque = () => {
// These are computed style values, so we don't need to worry about non-float values
// for `opacity`, here
// @TODO for definitive checks, traverse up the node tree from the inline menu container;
// nodes can exist between `html` and `body`
const htmlElement = globalThis.document.querySelector("html");
const bodyElement = globalThis.document.querySelector("body");
/**
* `querySelectorAll` for (non-standard) cases where the page has additional copies of
* page nodes that should be unique
*/
const pageElements = globalThis.document.querySelectorAll("html, body");
if (!htmlElement || !bodyElement) {
if (!pageElements.length) {
return false;
}
const htmlOpacity = globalThis.window.getComputedStyle(htmlElement)?.opacity || "0";
const bodyOpacity = globalThis.window.getComputedStyle(bodyElement)?.opacity || "0";
return [...pageElements].every((element) => {
// These are computed style values, so we don't need to worry about non-float values
// for `opacity`, here
const elementOpacity = globalThis.window.getComputedStyle(element)?.opacity || "0";
// Any value above this is considered "opaque" for our purposes
const opacityThreshold = 0.6;
// Any value above this is considered "opaque" for our purposes
const opacityThreshold = 0.6;
return parseFloat(htmlOpacity) > opacityThreshold && parseFloat(bodyOpacity) > opacityThreshold;
return parseFloat(elementOpacity) > opacityThreshold;
});
};
/**