fix(extension): remove webNavigation permission

This commit is contained in:
Timo
2026-08-17 18:41:40 +02:00
parent 76adafd1e4
commit 4d7897028b
7 changed files with 30 additions and 86 deletions
+2 -18
View File
@@ -2190,7 +2190,7 @@ function setPageApiSeekEnabled(enabled) {
}
async function deactivateMediaFrameMonitors(tabId) {
const targets = await listMediaFrameScriptTargets(chrome, tabId);
const targets = listMediaFrameScriptTargets(tabId);
await Promise.all(targets.map(async target => {
const documentId = target.documentIds?.[0];
const frameId = target.frameIds?.[0];
@@ -2293,7 +2293,7 @@ function createTargetActivationSupersededError() {
}
async function injectMediaFrameMonitors(tabId, contentTarget) {
const targets = await listMediaFrameScriptTargets(chrome, tabId);
const targets = listMediaFrameScriptTargets(tabId);
let injectedCount = 0;
await Promise.all(targets.map(async target => {
try {
@@ -2923,22 +2923,6 @@ if (chrome.permissions?.onAdded?.addListener) {
});
}
if (chrome.webNavigation?.onCompleted?.addListener) {
chrome.webNavigation.onCompleted.addListener((details) => {
ensureState().then(async () => {
const tabId = normalizeTabId(details?.tabId);
const frameId = normalizeFrameId(details?.frameId);
if (tabId === null
|| tabId !== normalizeTabId(currentTabId)
|| frameId === 0
|| (frameId !== normalizeFrameId(currentTargetFrameId) && currentTargetHasVideo)) {
return;
}
await refreshCurrentMediaTarget(tabId, { queueIfRunning: true });
}).catch(error => addLog(`Media frame navigation refresh failed: ${error.message}`, 'warn'));
});
}
if (chrome.tabs?.onRemoved?.addListener) {
chrome.tabs.onRemoved.addListener((removedTabId) => {
ensureState().then(async () => {
+1 -2
View File
@@ -11,8 +11,7 @@
"scripting",
"alarms",
"activeTab",
"notifications",
"webNavigation"
"notifications"
],
"host_permissions": [
"<all_urls>"
+2 -16
View File
@@ -427,21 +427,7 @@ function contentTarget(tabId, selected) {
};
}
export async function listMediaFrameScriptTargets(chromeApi, tabId) {
if (chromeApi.webNavigation?.getAllFrames) {
try {
const frames = await chromeApi.webNavigation.getAllFrames({ tabId });
if (Array.isArray(frames) && frames.length > 0) {
return frames
.filter(frame => Number.isInteger(frame?.frameId))
.map(frame => (typeof frame.documentId === 'string' && frame.documentId
? { tabId, documentIds: [frame.documentId] }
: { tabId, frameIds: [frame.frameId] }));
}
} catch {
// Older browsers fall back to the all-frames probe below.
}
}
export function listMediaFrameScriptTargets(tabId) {
return [{ tabId, allFrames: true }];
}
@@ -466,7 +452,7 @@ export async function resolveMediaContentTarget(chromeApi, tabId, {
let ambiguous = false;
for (let attempt = 0; attempt < attempts; attempt++) {
const scriptTargets = await listMediaFrameScriptTargets(chromeApi, tabId);
const scriptTargets = listMediaFrameScriptTargets(tabId);
let results = await executeInAccessibleFrames(
chromeApi,
scriptTargets,
+6 -42
View File
@@ -155,55 +155,19 @@ describe('cross-origin media-frame targeting', () => {
});
});
it('keeps an accessible player when an unrelated child frame is denied', async () => {
const top = frame(0, {
bestVideo: null,
videoCount: 0,
embeddedFrames: [
{
href: 'https://player-8.example/embed',
origin: 'https://player-8.example',
area: 830 * 498,
width: 830,
height: 498,
visible: true,
mediaHint: true
},
{
href: 'https://widget-denied.example/frame',
origin: 'https://widget-denied.example',
area: 300 * 250,
width: 300,
height: 250,
visible: true,
mediaHint: false
}
]
});
const player = frame(8);
const getAllFrames = vi.fn().mockResolvedValue([
{ frameId: 0, documentId: 'document-0' },
{ frameId: 8, documentId: 'document-8' },
{ frameId: 9, documentId: 'document-9' }
]);
const executeScript = vi.fn().mockImplementation(async ({ target, func }) => {
const documentId = target.documentIds?.[0];
if (documentId === 'document-9') throw new Error('Cannot access contents of the page');
if (func?.name !== 'inspectMediaFrame') return [];
return documentId === 'document-8' ? [player] : [top];
});
it('uses all-frame probing without a navigation permission', async () => {
const executeScript = vi.fn().mockResolvedValue([frame(8)]);
await expect(resolveMediaContentTarget(
{ scripting: { executeScript }, webNavigation: { getAllFrames } },
{ scripting: { executeScript } },
42,
{ attempts: 1, probeDelayMs: 0 }
)).resolves.toMatchObject({
frameId: 8,
documentId: 'document-8',
hasVideo: true
hasVideo: true,
scriptTarget: { tabId: 42, documentIds: ['document-8'] }
});
expect(executeScript.mock.calls.some(([options]) => options.target.allFrames === true)).toBe(false);
expect(executeScript.mock.calls.some(([options]) => options.target.documentIds?.[0] === 'document-9')).toBe(true);
expect(executeScript.mock.calls[0][0].target).toEqual({ tabId: 42, allFrames: true });
});
it('does not trust parent visibility from an older probe token', () => {
+12 -5
View File
@@ -39,14 +39,14 @@ describe('target tab lifecycle', () => {
it('removes monitors injected by a superseded cross-tab activation', () => {
expect(backgroundSource).toContain('function isTargetActivationSuperseded(tabId, activationGeneration)');
expect(backgroundSource).toContain('activationGeneration\n });');
expect(backgroundSource).toMatch(/navigationRetries: navigationRetries - 1,\s*activationGeneration\s*\}\)/);
expect(backgroundSource).toMatch(/await injectMediaFrameMonitors\(tabId, contentTarget\);[\s\S]*if \(isTargetActivationSuperseded\(tabId, activationGeneration\)\)[\s\S]*await deactivateMediaFrameMonitors\(tabId\);/);
expect(backgroundSource).toContain("error.code = 'target_activation_superseded'");
});
it('binds cross-origin targets to an exact document and monitors every accessible frame', () => {
it('uses all-frame probing for cross-origin targets without navigation permissions', () => {
expect(backgroundSource).toContain("files: ['media-frame-monitor.js']");
expect(backgroundSource).toContain('const targets = await listMediaFrameScriptTargets(chrome, tabId)');
expect(backgroundSource).toContain('const targets = listMediaFrameScriptTargets(tabId)');
expect(backgroundSource).toContain('One denied widget frame must not block the selected player');
expect(backgroundSource).toContain("navigationError.code = 'media_target_navigated'");
expect(backgroundSource).toContain("{ type: 'MEDIA_MONITOR_DEACTIVATE' }");
@@ -57,8 +57,15 @@ describe('target tab lifecycle', () => {
expect(monitorSource).toContain('if (!force && nextSignature === lastCandidateSignature) return');
expect(monitorSource).toContain("const MEDIA_STATE_EVENTS = ['play', 'pause', 'loadedmetadata'");
expect(monitorSource).toContain("node.querySelector?.('video, iframe, frame')");
expect(manifest.permissions).toContain('webNavigation');
expect(backgroundSource).toContain('chrome.webNavigation.onCompleted.addListener');
expect(manifest.permissions).toEqual([
'storage',
'tabs',
'scripting',
'alarms',
'activeTab',
'notifications'
]);
expect(backgroundSource).not.toMatch(/chrome\.(?:web)?Navigation/);
});
it('serializes content commands and coalesces target refreshes', () => {
+5 -2
View File
@@ -58,8 +58,11 @@ async function getFrameMonitorState(context, extensionId, pageUrl, frameUrlPart)
return withExtensionPage(context, extensionId, page => page.evaluate(async ({ pageUrl, frameUrlPart }) => {
const [tab] = await chrome.tabs.query({ url: pageUrl });
if (!tab) throw new Error(`no tab matched ${pageUrl}`);
const frames = await chrome.webNavigation.getAllFrames({ tabId: tab.id });
const frame = frames.find(candidate => candidate.url.includes(frameUrlPart));
const frameResults = await chrome.scripting.executeScript({
target: { tabId: tab.id, allFrames: true },
func: () => ({ href: location.href, cleanup: typeof window.__koalaMediaFrameMonitorCleanup })
});
const frame = frameResults.find(candidate => candidate.result?.href.includes(frameUrlPart));
if (!frame) throw new Error(`no frame matched ${frameUrlPart}`);
const target = frame.documentId
? { tabId: tab.id, documentIds: [frame.documentId] }
+2 -1
View File
@@ -1,4 +1,5 @@
import { defineConfig } from '@playwright/test';
import { fileURLToPath } from 'node:url';
const PORT = Number(process.env.KOALA_E2E_PORT || 4173);
@@ -25,7 +26,7 @@ export default defineConfig({
}
},
webServer: {
command: `node ${new URL('./fixture-server.mjs', import.meta.url).pathname} ${PORT}`,
command: `node "${fileURLToPath(new URL('./fixture-server.mjs', import.meta.url))}" ${PORT}`,
url: `http://localhost:${PORT}/pages/simple-player.html`,
reuseExistingServer: !process.env.CI,
stdout: 'ignore',