mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-22 08:56:36 +00:00
fix(extension): remove webNavigation permission
This commit is contained in:
+2
-18
@@ -2190,7 +2190,7 @@ function setPageApiSeekEnabled(enabled) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deactivateMediaFrameMonitors(tabId) {
|
async function deactivateMediaFrameMonitors(tabId) {
|
||||||
const targets = await listMediaFrameScriptTargets(chrome, tabId);
|
const targets = listMediaFrameScriptTargets(tabId);
|
||||||
await Promise.all(targets.map(async target => {
|
await Promise.all(targets.map(async target => {
|
||||||
const documentId = target.documentIds?.[0];
|
const documentId = target.documentIds?.[0];
|
||||||
const frameId = target.frameIds?.[0];
|
const frameId = target.frameIds?.[0];
|
||||||
@@ -2293,7 +2293,7 @@ function createTargetActivationSupersededError() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function injectMediaFrameMonitors(tabId, contentTarget) {
|
async function injectMediaFrameMonitors(tabId, contentTarget) {
|
||||||
const targets = await listMediaFrameScriptTargets(chrome, tabId);
|
const targets = listMediaFrameScriptTargets(tabId);
|
||||||
let injectedCount = 0;
|
let injectedCount = 0;
|
||||||
await Promise.all(targets.map(async target => {
|
await Promise.all(targets.map(async target => {
|
||||||
try {
|
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) {
|
if (chrome.tabs?.onRemoved?.addListener) {
|
||||||
chrome.tabs.onRemoved.addListener((removedTabId) => {
|
chrome.tabs.onRemoved.addListener((removedTabId) => {
|
||||||
ensureState().then(async () => {
|
ensureState().then(async () => {
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
"scripting",
|
"scripting",
|
||||||
"alarms",
|
"alarms",
|
||||||
"activeTab",
|
"activeTab",
|
||||||
"notifications",
|
"notifications"
|
||||||
"webNavigation"
|
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
|
|||||||
@@ -427,21 +427,7 @@ function contentTarget(tabId, selected) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listMediaFrameScriptTargets(chromeApi, tabId) {
|
export function listMediaFrameScriptTargets(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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [{ tabId, allFrames: true }];
|
return [{ tabId, allFrames: true }];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,7 +452,7 @@ export async function resolveMediaContentTarget(chromeApi, tabId, {
|
|||||||
let ambiguous = false;
|
let ambiguous = false;
|
||||||
|
|
||||||
for (let attempt = 0; attempt < attempts; attempt++) {
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
||||||
const scriptTargets = await listMediaFrameScriptTargets(chromeApi, tabId);
|
const scriptTargets = listMediaFrameScriptTargets(tabId);
|
||||||
let results = await executeInAccessibleFrames(
|
let results = await executeInAccessibleFrames(
|
||||||
chromeApi,
|
chromeApi,
|
||||||
scriptTargets,
|
scriptTargets,
|
||||||
|
|||||||
@@ -155,55 +155,19 @@ describe('cross-origin media-frame targeting', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps an accessible player when an unrelated child frame is denied', async () => {
|
it('uses all-frame probing without a navigation permission', async () => {
|
||||||
const top = frame(0, {
|
const executeScript = vi.fn().mockResolvedValue([frame(8)]);
|
||||||
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];
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(resolveMediaContentTarget(
|
await expect(resolveMediaContentTarget(
|
||||||
{ scripting: { executeScript }, webNavigation: { getAllFrames } },
|
{ scripting: { executeScript } },
|
||||||
42,
|
42,
|
||||||
{ attempts: 1, probeDelayMs: 0 }
|
{ attempts: 1, probeDelayMs: 0 }
|
||||||
)).resolves.toMatchObject({
|
)).resolves.toMatchObject({
|
||||||
frameId: 8,
|
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[0][0].target).toEqual({ tabId: 42, allFrames: true });
|
||||||
expect(executeScript.mock.calls.some(([options]) => options.target.documentIds?.[0] === 'document-9')).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not trust parent visibility from an older probe token', () => {
|
it('does not trust parent visibility from an older probe token', () => {
|
||||||
|
|||||||
@@ -39,14 +39,14 @@ describe('target tab lifecycle', () => {
|
|||||||
|
|
||||||
it('removes monitors injected by a superseded cross-tab activation', () => {
|
it('removes monitors injected by a superseded cross-tab activation', () => {
|
||||||
expect(backgroundSource).toContain('function isTargetActivationSuperseded(tabId, activationGeneration)');
|
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).toMatch(/await injectMediaFrameMonitors\(tabId, contentTarget\);[\s\S]*if \(isTargetActivationSuperseded\(tabId, activationGeneration\)\)[\s\S]*await deactivateMediaFrameMonitors\(tabId\);/);
|
||||||
expect(backgroundSource).toContain("error.code = 'target_activation_superseded'");
|
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("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('One denied widget frame must not block the selected player');
|
||||||
expect(backgroundSource).toContain("navigationError.code = 'media_target_navigated'");
|
expect(backgroundSource).toContain("navigationError.code = 'media_target_navigated'");
|
||||||
expect(backgroundSource).toContain("{ type: 'MEDIA_MONITOR_DEACTIVATE' }");
|
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('if (!force && nextSignature === lastCandidateSignature) return');
|
||||||
expect(monitorSource).toContain("const MEDIA_STATE_EVENTS = ['play', 'pause', 'loadedmetadata'");
|
expect(monitorSource).toContain("const MEDIA_STATE_EVENTS = ['play', 'pause', 'loadedmetadata'");
|
||||||
expect(monitorSource).toContain("node.querySelector?.('video, iframe, frame')");
|
expect(monitorSource).toContain("node.querySelector?.('video, iframe, frame')");
|
||||||
expect(manifest.permissions).toContain('webNavigation');
|
expect(manifest.permissions).toEqual([
|
||||||
expect(backgroundSource).toContain('chrome.webNavigation.onCompleted.addListener');
|
'storage',
|
||||||
|
'tabs',
|
||||||
|
'scripting',
|
||||||
|
'alarms',
|
||||||
|
'activeTab',
|
||||||
|
'notifications'
|
||||||
|
]);
|
||||||
|
expect(backgroundSource).not.toMatch(/chrome\.(?:web)?Navigation/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('serializes content commands and coalesces target refreshes', () => {
|
it('serializes content commands and coalesces target refreshes', () => {
|
||||||
|
|||||||
@@ -58,8 +58,11 @@ async function getFrameMonitorState(context, extensionId, pageUrl, frameUrlPart)
|
|||||||
return withExtensionPage(context, extensionId, page => page.evaluate(async ({ pageUrl, frameUrlPart }) => {
|
return withExtensionPage(context, extensionId, page => page.evaluate(async ({ pageUrl, frameUrlPart }) => {
|
||||||
const [tab] = await chrome.tabs.query({ url: pageUrl });
|
const [tab] = await chrome.tabs.query({ url: pageUrl });
|
||||||
if (!tab) throw new Error(`no tab matched ${pageUrl}`);
|
if (!tab) throw new Error(`no tab matched ${pageUrl}`);
|
||||||
const frames = await chrome.webNavigation.getAllFrames({ tabId: tab.id });
|
const frameResults = await chrome.scripting.executeScript({
|
||||||
const frame = frames.find(candidate => candidate.url.includes(frameUrlPart));
|
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}`);
|
if (!frame) throw new Error(`no frame matched ${frameUrlPart}`);
|
||||||
const target = frame.documentId
|
const target = frame.documentId
|
||||||
? { tabId: tab.id, documentIds: [frame.documentId] }
|
? { tabId: tab.id, documentIds: [frame.documentId] }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineConfig } from '@playwright/test';
|
import { defineConfig } from '@playwright/test';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
const PORT = Number(process.env.KOALA_E2E_PORT || 4173);
|
const PORT = Number(process.env.KOALA_E2E_PORT || 4173);
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
webServer: {
|
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`,
|
url: `http://localhost:${PORT}/pages/simple-player.html`,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
stdout: 'ignore',
|
stdout: 'ignore',
|
||||||
|
|||||||
Reference in New Issue
Block a user