aboutsummaryrefslogtreecommitdiff
path: root/extensions
diff options
context:
space:
mode:
Diffstat (limited to 'extensions')
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/service_worker.js56
-rw-r--r--extensions/chrome-profile-bridge/index.ts2
2 files changed, 36 insertions, 22 deletions
diff --git a/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
index de4f340..0ceabda 100644
--- a/extensions/chrome-profile-bridge/browser-extension/service_worker.js
+++ b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
@@ -714,34 +714,48 @@ async function overlayBlockedError(tabId) {
);
}
-// Injected into the page by overlay.clear. Removes other extensions' iframes (including inside
-// open shadow roots), blurs the focused field, sends Escape and clicks the page root.
+// Injected into the page by overlay.clear, in the extension's isolated world: only there can
+// chrome.dom.openOrClosedShadowRoot read a closed shadow root, and password managers put their
+// frame in one (1Password: a <com-1password-uso> element with the frame inside; there is no iframe
+// in the page's own DOM). Removes every element whose shadow root holds another extension's
+// iframe, plus loose foreign iframes, then blurs the focused field and sends Escape.
function clearForeignOverlays(ownId) {
const removed = [];
- const visit = (root) => {
+ const isForeignFrame = (el) => el.tagName === "IFRAME" && /^chrome-extension:\/\//.test(el.src) && !el.src.startsWith(`chrome-extension://${ownId}/`);
+ const rootOf = (el) => {
+ try {
+ return (typeof chrome !== "undefined" && chrome.dom && chrome.dom.openOrClosedShadowRoot ? chrome.dom.openOrClosedShadowRoot(el) : null) || el.shadowRoot;
+ } catch {
+ return el.shadowRoot;
+ }
+ };
+ // Returns the foreign frame URLs found inside `root`, looking through nested shadow roots.
+ const framesIn = (root) => {
+ const found = [];
for (const el of Array.from(root.querySelectorAll("*"))) {
- if (el.tagName === "IFRAME" && /^chrome-extension:\/\//.test(el.src) && !el.src.startsWith(`chrome-extension://${ownId}/`)) {
- removed.push(el.src.replace(/[?#].*$/, ""));
- el.remove();
- } else if (el.shadowRoot) {
- visit(el.shadowRoot);
- }
+ if (isForeignFrame(el)) found.push(el.src.replace(/[?#].*$/, ""));
+ const inner = rootOf(el);
+ if (inner) found.push(...framesIn(inner));
}
+ return found;
};
- visit(document);
+ for (const el of Array.from(document.querySelectorAll("*"))) {
+ if (!el.isConnected) continue;
+ if (isForeignFrame(el)) {
+ removed.push(el.src.replace(/[?#].*$/, ""));
+ el.remove();
+ continue;
+ }
+ const root = rootOf(el);
+ const frames = root ? framesIn(root) : [];
+ if (frames.length > 0) {
+ removed.push(`<${el.tagName.toLowerCase()}> holding ${frames.join(", ")}`);
+ el.remove();
+ }
+ }
const active = document.activeElement;
if (active && active !== document.body && typeof active.blur === "function") active.blur();
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", code: "Escape", bubbles: true }));
- // A click outside is what closes a password manager's inline menu (measured on 1Password: this,
- // not Escape or blur, is what freed the tab). Same event shape as domClickFallback, which worked,
- // sent to the page root so no page control is clicked.
- const root = document.documentElement;
- const init = { bubbles: true, cancelable: true, view: window, clientX: 900, clientY: 500, button: 0, buttons: 1 };
- root.dispatchEvent(new PointerEvent("pointerdown", { ...init, pointerId: 1, pointerType: "mouse", isPrimary: true }));
- root.dispatchEvent(new MouseEvent("mousedown", init));
- root.dispatchEvent(new PointerEvent("pointerup", { ...init, pointerId: 1, pointerType: "mouse", isPrimary: true, buttons: 0 }));
- root.dispatchEvent(new MouseEvent("mouseup", { ...init, buttons: 0 }));
- root.click();
const custom = Array.from(document.querySelectorAll("*")).filter((el) => el.tagName.includes("-")).map((el) => el.tagName.toLowerCase());
return { removed, seen: { iframes: document.querySelectorAll("iframe").length, customElements: [...new Set(custom)].slice(0, 10), active: document.activeElement?.tagName ?? null } };
}
@@ -750,7 +764,7 @@ async function clearOverlay(params) {
const tab = await getTabByParams(params, { createOwnedTarget: false });
let injected = { removed: [] };
try {
- const [first] = await executeScriptTimed({ target: { tabId: tab.id, frameIds: [0] }, world: "MAIN", func: clearForeignOverlays, args: [chrome.runtime.id] }, "chrome_clear_overlay");
+ const [first] = await executeScriptTimed({ target: { tabId: tab.id, frameIds: [0] }, func: clearForeignOverlays, args: [chrome.runtime.id] }, "chrome_clear_overlay");
injected = first?.result ?? injected;
} catch (error) {
throw new Error(`Could not run the clean-up on tab ${tab.id}: ${error?.message || error}`);
diff --git a/extensions/chrome-profile-bridge/index.ts b/extensions/chrome-profile-bridge/index.ts
index 4384967..aa86379 100644
--- a/extensions/chrome-profile-bridge/index.ts
+++ b/extensions/chrome-profile-bridge/index.ts
@@ -2286,7 +2286,7 @@ Usage rules:
name: "chrome_clear_overlay",
label: "Chrome Clear Overlay",
description:
- "Close another extension's prompt that is blocking a tab, usually a password manager's (1Password) inline sign-in or autofill prompt. While such a prompt is open Chrome refuses the debugger on that tab, so screenshots, clicks, typing and evaluate fail with \"Another extension ... has a prompt open on this tab\". Decide for yourself whether to use it: if the prompt is just in your way (it popped up on a field you are filling, or you do not need anything from it), clear it. If you need the user to sign in, authorize or approve something with it, or the user may be in the middle of using it, do not clear it: tell the user what you are waiting for. It removes the other extension's frame, blurs the focused field, sends Escape and clicks the page body (a click outside is what closes 1Password's inline menu), then reports whether the tab is usable again. It does not touch the page's own content, and it cannot close 1Password's browser-toolbar popup or the 1Password desktop app (if the result says the tab is still blocked, tell the user). Navigating the tab away also frees it.",
+ "Close another extension's prompt that is blocking a tab, usually a password manager's (1Password) inline sign-in or autofill prompt. While such a prompt is open Chrome refuses the debugger on that tab, so screenshots, clicks, typing and evaluate fail with \"Another extension ... has a prompt open on this tab\". Decide for yourself whether to use it: if the prompt is just in your way (it popped up on a field you are filling, or you do not need anything from it), clear it. If you need the user to sign in, authorize or approve something with it, or the user may be in the middle of using it, do not clear it: tell the user what you are waiting for. It removes the other extension's frame, blurs the focused field and sends Escape, then reports whether the tab is usable again. It does not touch the page's own content, and it cannot close 1Password's browser-toolbar popup or the 1Password desktop app (if the result says the tab is still blocked, tell the user). Navigating the tab away also frees it.",
promptSnippet: "Close a password manager prompt that blocks a tab, unless you are waiting for the user to use it.",
parameters: Type.Object({
targetId: Type.Optional(Type.String()),