diff options
Diffstat (limited to 'toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html')
-rw-r--r-- | toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html | 703 |
1 files changed, 703 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html new file mode 100644 index 0000000000..076c177dfa --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_activeTab.html @@ -0,0 +1,703 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for content script</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.manifestV3.enabled", true]], + }); +}); + +// Create a test extension with the provided function as the background +// script. The background script will have a few helpful functions +// available. +/* global awaitLoad, gatherFrameSources */ +function makeExtension({ + background, + useScriptingAPI = false, + manifest_version = 2, + host_permissions, +}) { + // Wait for a webNavigation.onCompleted event where the details for the + // loaded page match the attributes of `filter`. + function awaitLoad(filter) { + return new Promise(resolve => { + const listener = details => { + if (Object.keys(filter).every(key => details[key] === filter[key])) { + browser.webNavigation.onCompleted.removeListener(listener); + resolve(); + } + }; + browser.webNavigation.onCompleted.addListener(listener); + }); + } + + // Return a string with a (sorted) list of the source of all frames + // in the given tab into which this extension can inject scripts + // (ie all frames for which it has the activeTab permission). + // Source is the hostname for frames in http sources, or the full + // location href in other documents (eg about: pages) + const gatherFrameSources = useScriptingAPI ? + async function gatherFrameSources(tabid) { + let results = await browser.scripting.executeScript({ + target: { tabId: tabid, allFrames: true }, + func: () => window.location.hostname || window.location.href, + }); + // Adjust `result` so that it looks like the one returned by + // `tabs.executeScript()`. + let result = results.map(res => res.result); + + return String(result.sort()); + } : async function gatherFrameSources(tabid) { + let result = await browser.tabs.executeScript(tabid, { + allFrames: true, + matchAboutBlank: true, + code: "window.location.hostname || window.location.href;", + }); + + return String(result.sort()); + }; + + const permissions = ["webNavigation"]; + if (useScriptingAPI) { + permissions.push("scripting"); + } + + // When host_permissions is passed, test "automatic activeTab" for ungranted + // host_permissions in mv3, else test with the normal activeTab permission. + if (!host_permissions) { + permissions.push("activeTab"); + } + + return ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version, + permissions, + host_permissions, + }, + background: [ + `const useScriptingAPI = ${useScriptingAPI};`, + `const manifest_version = ${manifest_version};`, + `${awaitLoad}`, + `${gatherFrameSources}`, + `${ExtensionTestCommon.serializeScript(background)}`, + ].join("\n") + }); +} + +// Helper function to verify that executeScript() fails without the activeTab +// permission (or any specific origin permissions). +const verifyNoActiveTab = async ({ useScriptingAPI, manifest_version, host_permissions }) => { + let extension = makeExtension({ + async background() { + const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html"; + + let [tab] = await Promise.all([ + browser.tabs.create({url: URL}), + awaitLoad({frameId: 0}), + ]); + + await browser.test.assertRejects( + gatherFrameSources(tab.id), + /^Missing host permission/, + "executeScript should fail without activeTab permission" + ); + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("no-active-tab"); + }, + useScriptingAPI, + manifest_version, + host_permissions, + }); + + await extension.startup(); + await extension.awaitFinish("no-active-tab"); + await extension.unload(); +}; + +add_task(async function test_no_activeTab_tabs() { + await verifyNoActiveTab({ useScriptingAPI: false }); +}); + +add_task(async function test_no_activeTab_scripting() { + await verifyNoActiveTab({ useScriptingAPI: true }); +}); + +add_task(async function test_no_activeTab_scripting_mv3() { + await verifyNoActiveTab({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: null, + }); +}); + +add_task(async function test_no_activeTab_scripting_mv3_autoActiveTab() { + await verifyNoActiveTab({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: ["http://mochi.test/"], + }); +}); + +// Test helper to verify that dynamically created iframes do not get the +// activeTab permission, unless same-origin with the top page. +const verifyDynamicFrames = async ({ useScriptingAPI, manifest_version, host_permissions }) => { + let extension = makeExtension({ + async background() { + const BASE_HOST = "www.example.com"; + + let [tab] = await Promise.all([ + browser.tabs.create({url: `https://${BASE_HOST}/`}), + awaitLoad({frameId: 0}), + ]); + + function inject() { + let nframes = 4; + function frameLoaded() { + nframes--; + if (nframes == 0) { + browser.runtime.sendMessage("frames-loaded"); + } + } + + // about:blank + let frame = document.createElement("iframe"); + frame.addEventListener("load", frameLoaded, {once: true}); + document.body.appendChild(frame); + + let div = document.createElement("div"); + div.innerHTML = "<iframe src='https://test1.example.com/'></iframe>"; + let framelist = div.getElementsByTagName("iframe"); + browser.test.assertEq(1, framelist.length, "Found 1 frame inside div"); + framelist[0].addEventListener("load", frameLoaded, {once: true}); + document.body.appendChild(div); + + // about:srcdoc containing cross-origin frame. + let div2 = document.createElement("div"); + div2.innerHTML = "<iframe srcdoc=\"<iframe src='https://test2.example.com/'></iframe>\"></iframe>"; + framelist = div2.getElementsByTagName("iframe"); + browser.test.assertEq(1, framelist.length, "Found 1 frame inside div"); + framelist[0].addEventListener("load", frameLoaded, {once: true}); + document.body.appendChild(div2); + + // Note: URL's host is BASE_HOST (same as top). + const URL = "https://www.example.com/tests/toolkit/components/extensions/test/mochitest/file_contentscript_iframe.html"; + + let xhr = new XMLHttpRequest(); + xhr.open("GET", URL); + xhr.responseType = "document"; + xhr.overrideMimeType("text/html"); + + xhr.addEventListener("load", () => { + if (xhr.readyState != 4) { + return; + } + if (xhr.status != 200) { + browser.runtime.sendMessage("error"); + } + + let frame = xhr.response.getElementById("frame"); + browser.test.assertEq( + "https://test2.example.com/", + frame?.src, + "Found frame in response document with cross-origin URL" + ); + frame.addEventListener("load", frameLoaded, {once: true}); + document.body.appendChild(frame); + }, {once: true}); + xhr.addEventListener("error", () => { + browser.runtime.sendMessage("error"); + }, {once: true}); + xhr.send(); + } + + browser.test.onMessage.addListener(async msg => { + if (msg !== "go") { + browser.test.fail(`unexpected message received: ${msg}`); + return; + } + + let loadedPromise = new Promise((resolve, reject) => { + let listener = msg => { + let unlisten = () => browser.runtime.onMessage.removeListener(listener); + if (msg == "frames-loaded") { + unlisten(); + resolve(); + } else if (msg == "error") { + unlisten(); + reject(); + } + }; + browser.runtime.onMessage.addListener(listener); + }); + + if (useScriptingAPI) { + await browser.scripting.executeScript({ + target: { tabId: tab.id }, + func: inject, + }); + } else { + await browser.tabs.executeScript(tab.id, { + code: `(${inject})();`, + }); + } + + await loadedPromise; + + let result = await gatherFrameSources(tab.id); + + browser.test.assertEq( + String(["about:blank", "about:srcdoc", BASE_HOST]), + result, + `Script injected only into (same origin) about:blank-ish dynamically created frames` + ); + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("dynamic-frames"); + }); + + browser.test.sendMessage("ready", tab.id); + }, + useScriptingAPI, + manifest_version, + host_permissions, + }); + + await extension.startup(); + + let tabId = await extension.awaitMessage("ready"); + extension.grantActiveTab(tabId); + + extension.sendMessage("go"); + await extension.awaitFinish("dynamic-frames"); + + await extension.unload(); +}; + +add_task(async function test_dynamic_frames_tabs() { + await verifyDynamicFrames({ useScriptingAPI: false }); +}); + +add_task(async function test_dynamic_frames_scripting() { + await verifyDynamicFrames({ useScriptingAPI: true }); +}); + +add_task(async function test_dynamic_frames_scripting_mv3() { + await verifyDynamicFrames({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: null, + }); +}); + +add_task(async function test_dynamic_frames_scripting_mv3_autoActiveTab() { + await verifyDynamicFrames({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: ["https://www.example.com/"], + }); +}); + +// Test helper to verify that an iframe created from an <iframe srcdoc> gets +// the activeTab permission. +const verifySrcdoc = async ({ useScriptingAPI, manifest_version, host_permissions }) => { + let extension = makeExtension({ + async background() { + const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab2.html"; + const OUTER_SOURCE = "about:srcdoc"; + const PAGE_SOURCE = "mochi.test"; + const FRAME_SOURCE = "test1.example.com"; + + let [tab] = await Promise.all([ + browser.tabs.create({url: URL}), + awaitLoad({frameId: 0}), + ]); + + browser.test.onMessage.addListener(async msg => { + if (msg !== "go") { + browser.test.fail(`unexpected message received: ${msg}`); + return; + } + + let result = await gatherFrameSources(tab.id); + + if (manifest_version < 3) { + browser.test.assertEq( + String([OUTER_SOURCE, PAGE_SOURCE, FRAME_SOURCE]), + result, + "Script is injected into frame created from <iframe srcdoc>" + ); + } else { + browser.test.assertEq( + String([OUTER_SOURCE, PAGE_SOURCE]), + result, + "Script is not injected into cross-origin frame created from <iframe srcdoc>" + ); + } + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("srcdoc"); + }); + + browser.test.sendMessage("ready", tab.id); + }, + useScriptingAPI, + manifest_version, + host_permissions, + }); + + await extension.startup(); + + let tabId = await extension.awaitMessage("ready"); + extension.grantActiveTab(tabId); + + extension.sendMessage("go"); + await extension.awaitFinish("srcdoc"); + + await extension.unload(); +}; + +add_task(async function test_srcdoc_tabs() { + await verifySrcdoc({ useScriptingAPI: false }); +}); + +add_task(async function test_srcdoc_scripting() { + await verifySrcdoc({ useScriptingAPI: true }); +}); + +add_task(async function test_srcdoc_scripting_mv3() { + await verifySrcdoc({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: null, + }); +}); + +add_task(async function test_srcdoc_scripting_mv3_autoActiveTab() { + await verifySrcdoc({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: ["http://mochi.test/"], + }); +}); + +// Test helper to verify that navigating frames by setting the src attribute +// from the parent page does not grant the activeTab permission to the frame, +// unless the frame is same-origin to the top page. +const verifyNavigateBySrc = async ({ useScriptingAPI, manifest_version, host_permissions }) => { + let extension = makeExtension({ + async background() { + const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html"; + const PAGE_SOURCE = "mochi.test"; + const EMPTY_SOURCE = "about:blank"; + const FRAME_SOURCE = "test1.example.com"; + + let [tab] = await Promise.all([ + browser.tabs.create({url: URL}), + awaitLoad({frameId: 0}), + ]); + + browser.test.onMessage.addListener(async msg => { + if (msg !== "go") { + browser.test.fail(`unexpected message received: ${msg}`); + return; + } + + let result = await gatherFrameSources(tab.id); + if (manifest_version < 3) { + browser.test.assertEq( + String([EMPTY_SOURCE, PAGE_SOURCE, FRAME_SOURCE]), + result, + "In original page, script is injected into base page and original frames" + ); + } else { + browser.test.assertEq( + String([EMPTY_SOURCE, PAGE_SOURCE]), + result, + "In original page, script is injected into same-origin frames" + ); + } + + let loadedPromise = awaitLoad({tabId: tab.id}); + + let func = () => { + document.getElementById('emptyframe').src = 'http://test2.example.com/'; + }; + + if (useScriptingAPI) { + await browser.scripting.executeScript({ + target: { tabId: tab.id }, + func, + }); + } else { + await browser.tabs.executeScript(tab.id, { code: `(${func})();` }); + } + + await loadedPromise; + + + result = await gatherFrameSources(tab.id); + if (manifest_version < 3) { + browser.test.assertEq( + String([PAGE_SOURCE, FRAME_SOURCE]), + result, + "Script is not injected into initially empty frame after cross-origin navigation" + ); + } else { + browser.test.assertEq( + String([PAGE_SOURCE]), + result, + "Script is not injected into initially empty frame after cross-origin navigation" + ); + } + + loadedPromise = awaitLoad({tabId: tab.id}); + + func = () => { + document.getElementById('regularframe').src = 'http://mochi.test:8888/'; + }; + + if (useScriptingAPI) { + await browser.scripting.executeScript({ + target: { tabId: tab.id }, + func, + }); + } else { + await browser.tabs.executeScript(tab.id, { code: `(${func})();` }); + } + + await loadedPromise; + + result = await gatherFrameSources(tab.id); + + browser.test.assertEq( + String([PAGE_SOURCE, PAGE_SOURCE]), + result, + "Script injected into frame after navigating to same-origin" + ); + + await browser.tabs.remove(tab.id); + browser.test.notifyPass("test-scripts"); + }); + + browser.test.sendMessage("ready", tab.id); + }, + useScriptingAPI, + manifest_version, + host_permissions, + }); + + await extension.startup(); + + let tabId = await extension.awaitMessage("ready"); + extension.grantActiveTab(tabId); + + extension.sendMessage("go"); + await extension.awaitFinish("test-scripts"); + + await extension.unload(); +}; + +add_task(async function test_navigate_by_src_tabs() { + await verifyNavigateBySrc({ useScriptingAPI: false }); +}); + +add_task(async function test_navigate_by_src_scripting() { + await verifyNavigateBySrc({ useScriptingAPI: true }); +}); + +add_task(async function test_navigate_by_src_scripting_mv3() { + await verifyNavigateBySrc({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: null, + }); +}); + +add_task(async function test_navigate_by_src_scripting_mv3_autoActiveTab() { + await verifyNavigateBySrc({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: ["http://mochi.test/"], + }); +}); + +// Test helper to verify that navigating frames by setting window.location from +// inside the frame revokes the activeTab permission. +const verifyNavigateByWindowLocation = async ({ useScriptingAPI, manifest_version, host_permissions }) => { + let extension = makeExtension({ + async background() { + const URL = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contentscript_activeTab.html"; + const PAGE_SOURCE = "mochi.test"; + const EMPTY_SOURCE = "about:blank"; + const FRAME_SOURCE = "test1.example.com"; + + let [tab] = await Promise.all([ + browser.tabs.create({url: URL}), + awaitLoad({frameId: 0}), + ]); + + browser.test.onMessage.addListener(async msg => { + if (msg !== "go") { + browser.test.fail(`unexpected message received: ${msg}`); + return; + } + + let result = await gatherFrameSources(tab.id); + + if (manifest_version < 3) { + browser.test.assertEq( + String([EMPTY_SOURCE, PAGE_SOURCE, FRAME_SOURCE]), + result, + "Script initially injected into all frames" + ); + } else { + browser.test.assertEq( + String([EMPTY_SOURCE, PAGE_SOURCE]), + result, + "Script initially injected into all same-origin frames" + ); + } + + let nframes = 0; + let frames = await browser.webNavigation.getAllFrames({tabId: tab.id}); + for (let frame of frames) { + if (frame.parentFrameId == -1) { + continue; + } + + if (manifest_version >= 3 && frame.url.includes(FRAME_SOURCE)) { + // In MV3, can't access cross-origin iframes from the start. + + let invalidPromise = browser.scripting.executeScript({ + target: { tabId: tab.id, frameIds: [frame.frameId] }, + func: () => window.location.hostname, + }); + await browser.test.assertRejects( + invalidPromise, + /^Missing host permission for the tab or frames/, + "executeScript should fail on cross-origin frame" + ); + + continue; + } + + let loadPromise = awaitLoad({ + tabId: tab.id, + frameId: frame.frameId, + }); + + let func = () => { + window.location.href = 'https://test2.example.com/'; + }; + + if (useScriptingAPI) { + await browser.scripting.executeScript({ + target: { tabId: tab.id, frameIds: [frame.frameId] }, + func, + }); + } else { + await browser.tabs.executeScript(tab.id, { + frameId: frame.frameId, + matchAboutBlank: true, + code: `(${func})();`, + }); + } + + await loadPromise; + + let executePromise; + func = () => window.location.hostname; + + if (useScriptingAPI) { + executePromise = browser.scripting.executeScript({ + target: { tabId: tab.id, frameIds: [frame.frameId] }, + func, + }); + } else { + executePromise = browser.tabs.executeScript(tab.id, { + frameId: frame.frameId, + matchAboutBlank: true, + code: `(${func})();`, + }); + } + + await browser.test.assertRejects( + executePromise, + /^Missing host permission for the tab or frames/, + "executeScript should have failed on navigated frame" + ); + + nframes++; + } + + if (manifest_version < 3) { + browser.test.assertEq(2, nframes, "Found 2 frames"); + } else { + browser.test.assertEq(1, nframes, "Found 1 frame"); + } + + await browser.tabs.remove(tab.id); + browser.test.notifyPass("scripted-navigation"); + }); + + browser.test.sendMessage("ready", tab.id); + }, + useScriptingAPI, + manifest_version, + host_permissions, + }); + + await extension.startup(); + + let tabId = await extension.awaitMessage("ready"); + extension.grantActiveTab(tabId); + + extension.sendMessage("go"); + await extension.awaitFinish("scripted-navigation"); + + await extension.unload(); +}; + +add_task(async function test_navigate_by_window_location_tabs() { + await verifyNavigateByWindowLocation({ useScriptingAPI: false }); +}); + +add_task(async function test_navigate_by_window_location_scripting() { + await verifyNavigateByWindowLocation({ useScriptingAPI: true }); +}); + +add_task(async function test_navigate_by_window_location_scripting_mv3() { + await verifyNavigateByWindowLocation({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: null, + }); +}); + +add_task(async function test_navigate_by_window_location_scripting_mv3_autoActiveTab() { + await verifyNavigateByWindowLocation({ + useScriptingAPI: true, + manifest_version: 3, + host_permissions: ["http://mochi.test/"], + }); +}); + +</script> + +</body> +</html> |