Skip to content

Commit cfbbd2c

Browse files
committed
Bug 1774641 - Run content scripts on action click with ungranted host permission r=robwu
Differential Revision: https://phabricator.services.mozilla.com/D149621
1 parent da24bba commit cfbbd2c

10 files changed

+403
-15
lines changed

browser/components/extensions/test/browser/browser.ini

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ support-files =
3535
file_language_tlh.html
3636
file_dummy.html
3737
file_title.html
38+
file_with_xorigin_frame.html
39+
file_with_example_com_frame.html
3840
webNav_createdTarget.html
3941
webNav_createdTargetSource.html
4042
webNav_createdTargetSource_subframe.html
@@ -56,6 +58,7 @@ skip-if =
5658
[browser_ext_autocompletepopup.js]
5759
disabled = bug 1438663 # same focus issue as Bug 1438663
5860
[browser_ext_autoplayInBackground.js]
61+
[browser_ext_browserAction_activeScript.js]
5962
[browser_ext_browserAction_activeTab.js]
6063
[browser_ext_browserAction_area.js]
6164
[browser_ext_browserAction_experiment.js]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
"use strict";
2+
3+
const { ExtensionPermissions } = ChromeUtils.import(
4+
"resource://gre/modules/ExtensionPermissions.jsm"
5+
);
6+
7+
add_setup(async () => {
8+
await SpecialPowers.pushPrefEnv({
9+
set: [["extensions.manifestV3.enabled", true]],
10+
});
11+
});
12+
13+
function makeRunAtScript(runAt) {
14+
return `
15+
window.order ??= [];
16+
window.order.push("${runAt}");
17+
browser.test.sendMessage("injected", "order@" + window.order.join());
18+
`;
19+
}
20+
21+
async function makeExtension(id, manifest_version, granted) {
22+
info(`Loading extension ` + JSON.stringify({ id, granted }));
23+
24+
let manifest = {
25+
manifest_version,
26+
browser_specific_settings: { gecko: { id } },
27+
permissions: ["activeTab", "scripting"],
28+
content_scripts: [
29+
{
30+
matches: ["*://*/*"],
31+
js: ["static.js"],
32+
},
33+
],
34+
};
35+
36+
if (manifest_version === 3) {
37+
manifest.action = {};
38+
} else {
39+
manifest.browser_action = {};
40+
}
41+
42+
let ext = ExtensionTestUtils.loadExtension({
43+
manifest,
44+
useAddonManager: "temporary",
45+
46+
background() {
47+
browser.test.onMessage.addListener(async (msg, script) => {
48+
if (msg === "dynamic-script") {
49+
await browser.scripting.registerContentScripts([script]);
50+
browser.test.sendMessage("dynamic-script-done");
51+
} else if (msg === "injected-flush?") {
52+
browser.test.sendMessage("injected", "flush");
53+
}
54+
});
55+
56+
let action = browser.action || browser.browserAction;
57+
58+
action.onClicked.addListener(tab => {
59+
browser.scripting.executeScript({
60+
target: { tabId: tab.id },
61+
func: () => {
62+
// This needs to run after all scripts, to confirm the correct
63+
// number of scripts was injected. These two paths are inherently
64+
// independant, and since there's a variable number of content
65+
// scripts, there's no easy/better way to do it than a delay.
66+
requestAnimationFrame(() =>
67+
requestAnimationFrame(() => {
68+
let id = browser.runtime.id.split("@")[0];
69+
browser.test.sendMessage(
70+
"scriptCount",
71+
document.body.dataset[id]
72+
);
73+
})
74+
);
75+
},
76+
});
77+
});
78+
},
79+
80+
files: {
81+
"static.js"() {
82+
// Need to use DOM attributes (or the dataset), because two different
83+
// content script sandboxes (from top frame and the same-origin iframe)
84+
// get different wrappers, they don't see each other's top expandos.
85+
86+
// Need to avoid using the @ character (from the extension id)
87+
// because it's not allowed as part of the DOM attribute name.
88+
89+
let id = browser.runtime.id.split("@")[0];
90+
top.document.body.dataset[id] = (top.document.body.dataset[id] | 0) + 1;
91+
92+
browser.test.log(
93+
`Static content script from ${id} running on ${location.href}.`
94+
);
95+
96+
browser.test.sendMessage("injected", "static@" + location.host);
97+
},
98+
"dynamic.js"() {
99+
let id = browser.runtime.id.split("@")[0];
100+
top.document.body.dataset[id] = (top.document.body.dataset[id] | 0) + 1;
101+
102+
browser.test.log(
103+
`Dynamic content script from ${id} running on ${location.href}.`
104+
);
105+
106+
let frame = window === top ? "top" : "frame";
107+
browser.test.sendMessage(
108+
"injected",
109+
`dynamic-${frame}@${location.host}`
110+
);
111+
},
112+
"document_start.js": makeRunAtScript("document_start"),
113+
"document_end.js": makeRunAtScript("document_end"),
114+
"document_idle.js": makeRunAtScript("document_idle"),
115+
},
116+
});
117+
118+
if (granted) {
119+
info("Granting initial permissions.");
120+
await ExtensionPermissions.add(id, { permissions: [], origins: granted });
121+
}
122+
123+
await ext.startup();
124+
return ext;
125+
}
126+
127+
async function testActiveScript(extension, expectCount, expectHosts) {
128+
info(`Testing ${extension.id} on ${gBrowser.currentURI.spec}.`);
129+
130+
await clickBrowserAction(extension);
131+
132+
let received = [];
133+
for (let host of expectHosts) {
134+
info(`Waiting for a script to run in a ${host} frame.`);
135+
received.push(await extension.awaitMessage("injected"));
136+
}
137+
138+
extension.sendMessage("injected-flush?");
139+
info("Waiting for the flush message between test runs.");
140+
let flush = await extension.awaitMessage("injected");
141+
is(flush, "flush", "Messages properly flushed.");
142+
143+
is(received.sort().join(), expectHosts.join(), "All messages received.");
144+
145+
info(`Awaiting the counter from the activeTab content script.`);
146+
let scriptCount = await extension.awaitMessage("scriptCount");
147+
is(scriptCount | 0, expectCount, "Expected number of scripts running");
148+
}
149+
150+
add_task(async function test_action_activeScript() {
151+
// Static MV2 extension content scripts are not affected.
152+
let ext0 = await makeExtension("ext0@test", 2, ["*://example.com/*"]);
153+
154+
let ext1 = await makeExtension("ext1@test", 3);
155+
let ext2 = await makeExtension("ext2@test", 3, ["*://example.com/*"]);
156+
let ext3 = await makeExtension("ext3@test", 3, ["*://mochi.test/*"]);
157+
158+
// Test run_at script ordering.
159+
let ext4 = await makeExtension("ext4@test", 3);
160+
161+
await BrowserTestUtils.withNewTab("about:blank", async () => {
162+
info("No content scripts run on top level about:blank.");
163+
await testActiveScript(ext0, 0, []);
164+
await testActiveScript(ext1, 0, []);
165+
await testActiveScript(ext2, 0, []);
166+
await testActiveScript(ext3, 0, []);
167+
await testActiveScript(ext4, 0, []);
168+
});
169+
170+
let dynamicScript = {
171+
id: "script",
172+
js: ["dynamic.js"],
173+
matches: ["<all_urls>"],
174+
allFrames: true,
175+
persistAcrossSessions: false,
176+
};
177+
178+
// MV2 extensions don't support activeScript. This dynamic script won't run
179+
// when action button is clicked, but will run on example.com automatically.
180+
ext0.sendMessage("dynamic-script", dynamicScript);
181+
await ext0.awaitMessage("dynamic-script-done");
182+
183+
// Only ext3 will have a dynamic script, matching <all_urls> with allFrames.
184+
ext3.sendMessage("dynamic-script", dynamicScript);
185+
await ext3.awaitMessage("dynamic-script-done");
186+
187+
let url =
188+
"https://example.com/browser/browser/components/extensions/test/browser/file_with_xorigin_frame.html";
189+
190+
await BrowserTestUtils.withNewTab(url, async browser => {
191+
info("ext0 is MV2, static content script should run automatically.");
192+
info("ext0 has example.com permission, dynamic scripts should also run.");
193+
let received = [
194+
await ext0.awaitMessage("injected"),
195+
await ext0.awaitMessage("injected"),
196+
await ext0.awaitMessage("injected"),
197+
];
198+
is(
199+
received.sort().join(),
200+
"dynamic-frame@example.com,dynamic-top@example.com,static@example.com",
201+
"All messages received"
202+
);
203+
204+
info("Clicking ext0 button should not run content script again.");
205+
await testActiveScript(ext0, 3, []);
206+
207+
info("ext2 has host permission, content script should run automatically.");
208+
let static2 = await ext2.awaitMessage("injected");
209+
is(static2, "static@example.com", "Script ran automatically");
210+
211+
info("Clicking ext2 button should not run content script again.");
212+
await testActiveScript(ext2, 1, []);
213+
214+
await testActiveScript(ext1, 1, ["static@example.com"]);
215+
216+
await testActiveScript(ext3, 3, [
217+
"dynamic-frame@example.com",
218+
"dynamic-top@example.com",
219+
"static@example.com",
220+
]);
221+
222+
await testActiveScript(ext4, 1, ["static@example.com"]);
223+
224+
// Navigate same-origin iframe to another page, activeScripts shouldn't run.
225+
let bc = browser.browsingContext.children[0].children[0];
226+
SpecialPowers.spawn(bc, [], () => {
227+
content.location.href = "file_dummy.html";
228+
});
229+
// But dynamic script from ext0 should run automatically again.
230+
let dynamic0 = await ext0.awaitMessage("injected");
231+
is(dynamic0, "dynamic-frame@example.com", "Script ran automatically");
232+
233+
info("Clicking all buttons again should not activeScripts.");
234+
await testActiveScript(ext0, 4, []);
235+
await testActiveScript(ext1, 1, []);
236+
await testActiveScript(ext2, 1, []);
237+
// Except ext3 dynamic allFrames script runs in the new navigated page.
238+
await testActiveScript(ext3, 4, ["dynamic-frame@example.com"]);
239+
await testActiveScript(ext4, 1, []);
240+
});
241+
242+
// Register run_at content scripts in reverse order.
243+
for (let runAt of ["document_idle", "document_end", "document_start"]) {
244+
ext4.sendMessage("dynamic-script", {
245+
id: runAt,
246+
runAt: runAt,
247+
js: [`${runAt}.js`],
248+
matches: ["http://mochi.test/*"],
249+
persistAcrossSessions: false,
250+
});
251+
await ext4.awaitMessage("dynamic-script-done");
252+
}
253+
254+
await BrowserTestUtils.withNewTab("http://mochi.test:8888/", async () => {
255+
info("ext0 is MV2, static content script should run automatically.");
256+
let static0 = await ext0.awaitMessage("injected");
257+
is(static0, "static@mochi.test:8888", "Script ran automatically.");
258+
259+
info("Clicking ext0 button should not run content script again.");
260+
await testActiveScript(ext0, 1, []);
261+
262+
info("ext3 has host permission, content script should run automatically.");
263+
let received3 = [
264+
await ext3.awaitMessage("injected"),
265+
await ext3.awaitMessage("injected"),
266+
];
267+
is(
268+
received3.sort().join(),
269+
"dynamic-top@mochi.test:8888,static@mochi.test:8888",
270+
"All messages received."
271+
);
272+
273+
info("Clicking ext3 button should not run content script again.");
274+
await testActiveScript(ext3, 2, []);
275+
276+
await testActiveScript(ext1, 1, ["static@mochi.test:8888"]);
277+
await testActiveScript(ext2, 1, ["static@mochi.test:8888"]);
278+
279+
// Expect run_at content scripts to run in the correct order.
280+
await testActiveScript(ext4, 1, [
281+
"order@document_start",
282+
"order@document_start,document_end",
283+
"order@document_start,document_end,document_idle",
284+
"static@mochi.test:8888",
285+
]);
286+
287+
info("Clicking all buttons again should not run content scripts.");
288+
await testActiveScript(ext0, 1, []);
289+
await testActiveScript(ext1, 1, []);
290+
await testActiveScript(ext2, 1, []);
291+
await testActiveScript(ext3, 2, []);
292+
await testActiveScript(ext4, 1, []);
293+
});
294+
295+
await ext0.unload();
296+
await ext1.unload();
297+
await ext2.unload();
298+
await ext3.unload();
299+
await ext4.unload();
300+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!DOCTYPE HTML>
2+
<meta charset="utf-8">
3+
4+
Load an iframe from example.com <p>
5+
<iframe src="https://example.com/browser/browser/components/extensions/test/browser/context_frame.html"></iframe>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!DOCTYPE HTML>
2+
<meta charset="utf-8">
3+
4+
Load a cross-origin iframe from example.net <p>
5+
<iframe src="https://example.net/browser/browser/components/extensions/test/browser/file_with_example_com_frame.html"></iframe>

dom/chrome-webidl/WebExtensionContentScript.webidl

+5-3
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ interface MozDocumentMatcher {
2121
boolean matchesURI(URI uri);
2222

2323
/**
24-
* Returns true if the given window matches. This should be used
25-
* to determine whether to run a script in a window at load time.
24+
* Returns true if the given window matches. This should be used to
25+
* determine whether to run a script in a window at load time. Use
26+
* ignorePermissions to match without origin permissions in MV3.
2627
*/
27-
boolean matchesWindowGlobal(WindowGlobalChild windowGlobal);
28+
boolean matchesWindowGlobal(WindowGlobalChild windowGlobal,
29+
optional boolean ignorePermissions = false);
2830

2931
/**
3032
* If true, match all frames. If false, match only top-level frames.

toolkit/components/extensions/ExtensionActions.jsm

+1
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ class PanelActionBase {
222222
// existing preloaded activeTab permission.
223223
this.setActiveTabForPreload(null);
224224
this.extension.tabManager.addActiveTabPermission(tab);
225+
this.extension.tabManager.activateScripts(tab);
225226

226227
let popupUrl = this.getProperty(tab, "popup");
227228
// The "click" event is only dispatched when the popup is not shown. This

0 commit comments

Comments
 (0)