summaryrefslogtreecommitdiffstats
path: root/devtools/shared/commands/inspected-window
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/shared/commands/inspected-window/inspected-window-command.js145
-rw-r--r--devtools/shared/commands/inspected-window/moz.build10
-rw-r--r--devtools/shared/commands/inspected-window/tests/browser.ini20
-rw-r--r--devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window.js523
-rw-r--r--devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window_access.js315
-rw-r--r--devtools/shared/commands/inspected-window/tests/head.js12
-rw-r--r--devtools/shared/commands/inspected-window/tests/inspectedwindow-reload-target.sjs89
7 files changed, 1114 insertions, 0 deletions
diff --git a/devtools/shared/commands/inspected-window/inspected-window-command.js b/devtools/shared/commands/inspected-window/inspected-window-command.js
new file mode 100644
index 0000000000..0d15016ebd
--- /dev/null
+++ b/devtools/shared/commands/inspected-window/inspected-window-command.js
@@ -0,0 +1,145 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ getAdHocFrontOrPrimitiveGrip,
+ // eslint-disable-next-line mozilla/reject-some-requires
+} = require("resource://devtools/client/fronts/object.js");
+
+/**
+ * For now, this class is mostly a wrapper around webExtInspectedWindow actor.
+ */
+class InspectedWindowCommand {
+ constructor({ commands }) {
+ this.commands = commands;
+ }
+
+ /**
+ * Return a promise that resolves to the related target actor's front.
+ * The Web Extension inspected window actor.
+ *
+ * @return {Promise<WebExtensionInspectedWindowFront>}
+ */
+ getFront() {
+ return this.commands.targetCommand.targetFront.getFront(
+ "webExtensionInspectedWindow"
+ );
+ }
+
+ /**
+ * Evaluate the provided javascript code in a target window.
+ *
+ * @param {Object} webExtensionCallerInfo - The addonId and the url (the addon base url
+ * or the url of the actual caller filename and lineNumber) used to log useful
+ * debugging information in the produced error logs and eval stack trace.
+ * @param {String} expression - The expression to evaluate.
+ * @param {Object} options - An option object. Check the actor method definition to see
+ * what properties it can hold (minus the `consoleFront` property which is defined
+ * below).
+ * @param {WebConsoleFront} options.consoleFront - An optional webconsole front. When
+ * set, the result will be either a primitive, a LongStringFront or an
+ * ObjectFront, and the WebConsoleActor corresponding to the console front will
+ * be used to generate those, which is needed if we want to handle ObjectFronts
+ * on the client.
+ */
+ async eval(webExtensionCallerInfo, expression, options = {}) {
+ const { consoleFront } = options;
+
+ if (consoleFront) {
+ options.evalResultAsGrip = true;
+ options.toolboxConsoleActorID = consoleFront.actor;
+ delete options.consoleFront;
+ }
+
+ const front = await this.getFront();
+ const response = await front.eval(
+ webExtensionCallerInfo,
+ expression,
+ options
+ );
+
+ // If no consoleFront was provided, we can directly return the response.
+ if (!consoleFront) {
+ return response;
+ }
+
+ if (
+ !response.hasOwnProperty("exceptionInfo") &&
+ !response.hasOwnProperty("valueGrip")
+ ) {
+ throw new Error(
+ "Response does not have `exceptionInfo` or `valueGrip` property"
+ );
+ }
+
+ if (response.exceptionInfo) {
+ console.error(
+ response.exceptionInfo.description,
+ ...(response.exceptionInfo.details || [])
+ );
+ return response;
+ }
+
+ // On the server, the valueGrip is created from the toolbox webconsole actor.
+ // If we want since the ObjectFront connection is inherited from the parent front, we
+ // need to set the console front as the parent front.
+ return getAdHocFrontOrPrimitiveGrip(
+ response.valueGrip,
+ consoleFront || this
+ );
+ }
+
+ /**
+ * Reload the target tab, optionally bypass cache, customize the userAgent and/or
+ * inject a script in targeted document or any of its sub-frame.
+ *
+ * @param {WebExtensionCallerInfo} callerInfo
+ * the addonId and the url (the addon base url or the url of the actual caller
+ * filename and lineNumber) used to log useful debugging information in the
+ * produced error logs and eval stack trace.
+ * @param {Object} options
+ * @param {boolean|undefined} options.ignoreCache
+ * Enable/disable the cache bypass headers.
+ * @param {string|undefined} options.injectedScript
+ * Evaluate the provided javascript code in the top level and every sub-frame
+ * created during the page reload, before any other script in the page has been
+ * executed.
+ * @param {string|undefined} options.userAgent
+ * Customize the userAgent during the page reload.
+ * @returns {Promise} A promise that resolves once the page is done loading when userAgent
+ * or injectedScript option are passed. If those options are not provided, the
+ * Promise will resolve after the reload was initiated.
+ */
+ async reload(callerInfo, options = {}) {
+ if (this._reloadPending) {
+ return null;
+ }
+
+ this._reloadPending = true;
+
+ try {
+ // We always want to update the target configuration to set the user agent if one is
+ // passed, or to reset a potential existing override if userAgent isn't defined.
+ await this.commands.targetConfigurationCommand.updateConfiguration({
+ customUserAgent: options.userAgent,
+ });
+
+ const front = await this.getFront();
+ const result = await front.reload(callerInfo, options);
+ this._reloadPending = false;
+
+ return result;
+ } catch (e) {
+ this._reloadPending = false;
+ console.error(e);
+ return Promise.reject({
+ message: "An unexpected error occurred",
+ });
+ }
+ }
+}
+
+module.exports = InspectedWindowCommand;
diff --git a/devtools/shared/commands/inspected-window/moz.build b/devtools/shared/commands/inspected-window/moz.build
new file mode 100644
index 0000000000..2cf831d7b0
--- /dev/null
+++ b/devtools/shared/commands/inspected-window/moz.build
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "inspected-window-command.js",
+)
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser.ini"]
diff --git a/devtools/shared/commands/inspected-window/tests/browser.ini b/devtools/shared/commands/inspected-window/tests/browser.ini
new file mode 100644
index 0000000000..d3b5ce5fbc
--- /dev/null
+++ b/devtools/shared/commands/inspected-window/tests/browser.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+ !/devtools/client/shared/test/highlighter-test-actor.js
+ head.js
+ inspectedwindow-reload-target.sjs
+
+prefs =
+ # restrictedDomains must be set as early as possible, before the first use of
+ # the preference. browser_webextension_inspected_window_access.js relies on
+ # this pref to be set. We cannot use "prefs =" at the individual file, because
+ # another test in this manifest may already have resulted in browser startup.
+ extensions.webextensions.restrictedDomains=test2.example.com
+
+[browser_webextension_inspected_window.js]
+[browser_webextension_inspected_window_access.js]
+skip-if = http3 \ No newline at end of file
diff --git a/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window.js b/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window.js
new file mode 100644
index 0000000000..bf2b752e4d
--- /dev/null
+++ b/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window.js
@@ -0,0 +1,523 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_RELOAD_URL = `${URL_ROOT_SSL}/inspectedwindow-reload-target.sjs`;
+
+async function setup(pageUrl) {
+ // Disable bfcache for Fission for now.
+ // If Fission is disabled, the pref is no-op.
+ await SpecialPowers.pushPrefEnv({
+ set: [["fission.bfcacheInParent", false]],
+ });
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background() {
+ // This is just an empty extension used to ensure that the caller extension uuid
+ // actually exists.
+ },
+ });
+
+ await extension.startup();
+
+ const fakeExtCallerInfo = {
+ url: WebExtensionPolicy.getByID(extension.id).getURL(
+ "fake-caller-script.js"
+ ),
+ lineNumber: 1,
+ addonId: extension.id,
+ };
+
+ const tab = await addTab(pageUrl);
+
+ const commands = await CommandsFactory.forTab(tab, { isWebExtension: true });
+ await commands.targetCommand.startListening();
+
+ const webConsoleFront = await commands.targetCommand.targetFront.getFront(
+ "console"
+ );
+
+ return {
+ webConsoleFront,
+ commands,
+ extension,
+ fakeExtCallerInfo,
+ };
+}
+
+async function teardown({ commands, extension }) {
+ await commands.destroy();
+ gBrowser.removeCurrentTab();
+ await extension.unload();
+}
+
+function waitForNextTabNavigated(commands) {
+ const target = commands.targetCommand.targetFront;
+ return new Promise(resolve => {
+ target.on("tabNavigated", function tabNavigatedListener(pkt) {
+ if (pkt.state == "stop" && !pkt.isFrameSwitching) {
+ target.off("tabNavigated", tabNavigatedListener);
+ resolve();
+ }
+ });
+ });
+}
+
+// Script used as the injectedScript option in the inspectedWindow.reload tests.
+function injectedScript() {
+ if (!window.pageScriptExecutedFirst) {
+ window.addEventListener(
+ "DOMContentLoaded",
+ function () {
+ if (document.querySelector("pre")) {
+ document.querySelector("pre").textContent =
+ "injected script executed first";
+ }
+ },
+ { once: true }
+ );
+ }
+}
+
+// Script evaluated in the target tab, to collect the results of injectedScript
+// evaluation in the inspectedWindow.reload tests.
+function collectEvalResults() {
+ const results = [];
+ let iframeDoc = document;
+
+ while (iframeDoc) {
+ if (iframeDoc.querySelector("pre")) {
+ results.push(iframeDoc.querySelector("pre").textContent);
+ }
+ const iframe = iframeDoc.querySelector("iframe");
+ iframeDoc = iframe ? iframe.contentDocument : null;
+ }
+ return JSON.stringify(results);
+}
+
+add_task(async function test_successfull_inspectedWindowEval_result() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(URL_ROOT_SSL);
+
+ const result = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ "window.location",
+ {}
+ );
+
+ ok(result.value, "Got a result from inspectedWindow eval");
+ is(
+ result.value.href,
+ URL_ROOT_SSL,
+ "Got the expected window.location.href property value"
+ );
+ is(
+ result.value.protocol,
+ "https:",
+ "Got the expected window.location.protocol property value"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_successfull_inspectedWindowEval_resultAsGrip() {
+ const { commands, extension, fakeExtCallerInfo, webConsoleFront } =
+ await setup(URL_ROOT_SSL);
+
+ let result = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ "window",
+ {
+ evalResultAsGrip: true,
+ toolboxConsoleActorID: webConsoleFront.actor,
+ }
+ );
+
+ ok(result.valueGrip, "Got a result from inspectedWindow eval");
+ ok(result.valueGrip.actor, "Got a object actor as expected");
+ is(result.valueGrip.type, "object", "Got a value grip of type object");
+ is(
+ result.valueGrip.class,
+ "Window",
+ "Got a value grip which is instanceof Location"
+ );
+
+ // Test invalid evalResultAsGrip request.
+ result = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ "window",
+ {
+ evalResultAsGrip: true,
+ }
+ );
+
+ ok(
+ !result.value && !result.valueGrip,
+ "Got a null result from the invalid inspectedWindow eval call"
+ );
+ ok(
+ result.exceptionInfo.isError,
+ "Got an API Error result from inspectedWindow eval"
+ );
+ ok(
+ !result.exceptionInfo.isException,
+ "An error isException is false as expected"
+ );
+ is(
+ result.exceptionInfo.code,
+ "E_PROTOCOLERROR",
+ "Got the expected 'code' property in the error result"
+ );
+ is(
+ result.exceptionInfo.description,
+ "Inspector protocol error: %s - %s",
+ "Got the expected 'description' property in the error result"
+ );
+ is(
+ result.exceptionInfo.details.length,
+ 2,
+ "The 'details' array property should contains 1 element"
+ );
+ is(
+ result.exceptionInfo.details[0],
+ "Unexpected invalid sidebar panel expression request",
+ "Got the expected content in the error results's details"
+ );
+ is(
+ result.exceptionInfo.details[1],
+ "missing toolboxConsoleActorID",
+ "Got the expected content in the error results's details"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_error_inspectedWindowEval_result() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(URL_ROOT_SSL);
+
+ const result = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ "window",
+ {}
+ );
+
+ ok(!result.value, "Got a null result from inspectedWindow eval");
+ ok(
+ result.exceptionInfo.isError,
+ "Got an API Error result from inspectedWindow eval"
+ );
+ ok(
+ !result.exceptionInfo.isException,
+ "An error isException is false as expected"
+ );
+ is(
+ result.exceptionInfo.code,
+ "E_PROTOCOLERROR",
+ "Got the expected 'code' property in the error result"
+ );
+ is(
+ result.exceptionInfo.description,
+ "Inspector protocol error: %s",
+ "Got the expected 'description' property in the error result"
+ );
+ is(
+ result.exceptionInfo.details.length,
+ 1,
+ "The 'details' array property should contains 1 element"
+ );
+ ok(
+ result.exceptionInfo.details[0].includes("cyclic object value"),
+ "Got the expected content in the error results's details"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_exception_inspectedWindowEval_result() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(URL_ROOT_SSL);
+
+ const result = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ "throw Error('fake eval error');",
+ {}
+ );
+
+ ok(result.exceptionInfo.isException, "Got an exception as expected");
+ ok(!result.value, "Got an undefined eval value");
+ ok(!result.exceptionInfo.isError, "An exception should not be isError=true");
+ ok(
+ result.exceptionInfo.value.includes("Error: fake eval error"),
+ "Got the expected exception message"
+ );
+
+ const expectedCallerInfo = `called from ${fakeExtCallerInfo.url}:${fakeExtCallerInfo.lineNumber}`;
+ ok(
+ result.exceptionInfo.value.includes(expectedCallerInfo),
+ "Got the expected caller info in the exception message"
+ );
+
+ const expectedStack = `eval code:1:7`;
+ ok(
+ result.exceptionInfo.value.includes(expectedStack),
+ "Got the expected stack trace in the exception message"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_exception_inspectedWindowReload() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(
+ `${TEST_RELOAD_URL}?test=cache`
+ );
+
+ // Test reload with bypassCache=false.
+
+ const waitForNoBypassCacheReload = waitForNextTabNavigated(commands);
+ const reloadResult = await commands.inspectedWindowCommand.reload(
+ fakeExtCallerInfo,
+ {
+ ignoreCache: false,
+ }
+ );
+
+ ok(
+ !reloadResult,
+ "Got the expected undefined result from inspectedWindow reload"
+ );
+
+ await waitForNoBypassCacheReload;
+
+ const noBypassCacheEval = await commands.scriptCommand.execute(
+ "document.body.textContent"
+ );
+
+ is(
+ noBypassCacheEval.result,
+ "empty cache headers",
+ "Got the expected result with reload forceBypassCache=false"
+ );
+
+ // Test reload with bypassCache=true.
+
+ const waitForForceBypassCacheReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {
+ ignoreCache: true,
+ });
+
+ await waitForForceBypassCacheReload;
+
+ const forceBypassCacheEval = await commands.scriptCommand.execute(
+ "document.body.textContent"
+ );
+
+ is(
+ forceBypassCacheEval.result,
+ "no-cache:no-cache",
+ "Got the expected result with reload forceBypassCache=true"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_exception_inspectedWindowReload_customUserAgent() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(
+ `${TEST_RELOAD_URL}?test=user-agent`
+ );
+
+ // Test reload with custom userAgent.
+
+ const waitForCustomUserAgentReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {
+ userAgent: "Customized User Agent",
+ });
+
+ await waitForCustomUserAgentReload;
+
+ const customUserAgentEval = await commands.scriptCommand.execute(
+ "document.body.textContent"
+ );
+
+ is(
+ customUserAgentEval.result,
+ "Customized User Agent",
+ "Got the expected result on reload with a customized userAgent"
+ );
+
+ // Test reload with no custom userAgent.
+
+ const waitForNoCustomUserAgentReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {});
+
+ await waitForNoCustomUserAgentReload;
+
+ const noCustomUserAgentEval = await commands.scriptCommand.execute(
+ "document.body.textContent"
+ );
+
+ is(
+ noCustomUserAgentEval.result,
+ window.navigator.userAgent,
+ "Got the expected result with reload without a customized userAgent"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_exception_inspectedWindowReload_injectedScript() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(
+ `${TEST_RELOAD_URL}?test=injected-script&frames=3`
+ );
+
+ // Test reload with an injectedScript.
+
+ const waitForInjectedScriptReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {
+ injectedScript: `new ${injectedScript}`,
+ });
+ await waitForInjectedScriptReload;
+
+ const injectedScriptEval = await commands.scriptCommand.execute(
+ `(${collectEvalResults})()`
+ );
+
+ const expectedResult = new Array(5).fill("injected script executed first");
+
+ SimpleTest.isDeeply(
+ JSON.parse(injectedScriptEval.result),
+ expectedResult,
+ "Got the expected result on reload with an injected script"
+ );
+
+ // Test reload without an injectedScript.
+
+ const waitForNoInjectedScriptReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {});
+ await waitForNoInjectedScriptReload;
+
+ const noInjectedScriptEval = await commands.scriptCommand.execute(
+ `(${collectEvalResults})()`
+ );
+
+ const newExpectedResult = new Array(5).fill("injected script NOT executed");
+
+ SimpleTest.isDeeply(
+ JSON.parse(noInjectedScriptEval.result),
+ newExpectedResult,
+ "Got the expected result on reload with no injected script"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_exception_inspectedWindowReload_multiple_calls() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(
+ `${TEST_RELOAD_URL}?test=user-agent`
+ );
+
+ // Test reload with custom userAgent three times (and then
+ // check that only the first one has affected the page reload.
+
+ const waitForCustomUserAgentReload = waitForNextTabNavigated(commands);
+
+ commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {
+ userAgent: "Customized User Agent 1",
+ });
+ commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {
+ userAgent: "Customized User Agent 2",
+ });
+
+ await waitForCustomUserAgentReload;
+
+ const customUserAgentEval = await commands.scriptCommand.execute(
+ "document.body.textContent"
+ );
+
+ is(
+ customUserAgentEval.result,
+ "Customized User Agent 1",
+ "Got the expected result on reload with a customized userAgent"
+ );
+
+ // Test reload with no custom userAgent.
+
+ const waitForNoCustomUserAgentReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {});
+
+ await waitForNoCustomUserAgentReload;
+
+ const noCustomUserAgentEval = await commands.scriptCommand.execute(
+ "document.body.textContent"
+ );
+
+ is(
+ noCustomUserAgentEval.result,
+ window.navigator.userAgent,
+ "Got the expected result with reload without a customized userAgent"
+ );
+
+ await teardown({ commands, extension });
+});
+
+add_task(async function test_exception_inspectedWindowReload_stopped() {
+ const { commands, extension, fakeExtCallerInfo } = await setup(
+ `${TEST_RELOAD_URL}?test=injected-script&frames=3`
+ );
+
+ // Test reload on a page that calls window.stop() immediately during the page loading
+
+ const waitForPageLoad = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ "window.location += '&stop=windowStop'"
+ );
+
+ info("Load a webpage that calls 'window.stop()' while is still loading");
+ await waitForPageLoad;
+
+ info("Starting a reload with an injectedScript");
+ const waitForInjectedScriptReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {
+ injectedScript: `new ${injectedScript}`,
+ });
+ await waitForInjectedScriptReload;
+
+ const injectedScriptEval = await commands.scriptCommand.execute(
+ `(${collectEvalResults})()`
+ );
+
+ // The page should have stopped during the reload and only one injected script
+ // is expected.
+ const expectedResult = new Array(1).fill("injected script executed first");
+
+ SimpleTest.isDeeply(
+ JSON.parse(injectedScriptEval.result),
+ expectedResult,
+ "The injected script has been executed on the 'stopped' page reload"
+ );
+
+ // Reload again with no options.
+
+ info("Reload the tab again without any reload options");
+ const waitForNoInjectedScriptReload = waitForNextTabNavigated(commands);
+ await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {});
+ await waitForNoInjectedScriptReload;
+
+ const noInjectedScriptEval = await commands.scriptCommand.execute(
+ `(${collectEvalResults})()`
+ );
+
+ // The page should have stopped during the reload and no injected script should
+ // have been executed during this second reload (or it would mean that the previous
+ // customized reload was still pending and has wrongly affected the second reload)
+ const newExpectedResult = new Array(1).fill("injected script NOT executed");
+
+ SimpleTest.isDeeply(
+ JSON.parse(noInjectedScriptEval.result),
+ newExpectedResult,
+ "No injectedScript should have been evaluated during the second reload"
+ );
+
+ await teardown({ commands, extension });
+});
+
+// TODO: check eval with $0 binding once implemented (Bug 1300590)
diff --git a/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window_access.js b/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window_access.js
new file mode 100644
index 0000000000..3b32bb0aaa
--- /dev/null
+++ b/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window_access.js
@@ -0,0 +1,315 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function run_inspectedWindow_eval({ tab, codeToEval, extension }) {
+ const fakeExtCallerInfo = {
+ url: `moz-extension://${extension.uuid}/another/fake-caller-script.js`,
+ lineNumber: 1,
+ addonId: extension.id,
+ };
+ const commands = await CommandsFactory.forTab(tab, { isWebExtension: true });
+ await commands.targetCommand.startListening();
+ const result = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ codeToEval,
+ {}
+ );
+ await commands.destroy();
+ return result;
+}
+
+async function openAboutBlankTabWithExtensionOrigin(extension) {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ `moz-extension://${extension.uuid}/manifest.json`
+ );
+ const loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await ContentTask.spawn(tab.linkedBrowser, null, () => {
+ // about:blank inherits the principal when opened from content.
+ content.wrappedJSObject.location.assign("about:blank");
+ });
+ await loaded;
+ // Sanity checks:
+ is(tab.linkedBrowser.currentURI.spec, "about:blank", "expected tab");
+ is(
+ tab.linkedBrowser.contentPrincipal.originNoSuffix,
+ `moz-extension://${extension.uuid}`,
+ "about:blank should be at the extension origin"
+ );
+ return tab;
+}
+
+async function checkEvalResult({
+ extension,
+ description,
+ url,
+ createTab = () => BrowserTestUtils.openNewForegroundTab(gBrowser, url),
+ expectedResult,
+}) {
+ const tab = await createTab();
+ is(tab.linkedBrowser.currentURI.spec, url, "Sanity check: tab URL");
+ const result = await run_inspectedWindow_eval({
+ tab,
+ codeToEval: "'code executed at ' + location.href",
+ extension,
+ });
+ BrowserTestUtils.removeTab(tab);
+ SimpleTest.isDeeply(
+ result,
+ expectedResult,
+ `eval result for devtools.inspectedWindow.eval at ${url} (${description})`
+ );
+}
+
+async function checkEvalAllowed({ extension, description, url, createTab }) {
+ info(`checkEvalAllowed: ${description} (at URL: ${url})`);
+ await checkEvalResult({
+ extension,
+ description,
+ url,
+ createTab,
+ expectedResult: { value: `code executed at ${url}` },
+ });
+}
+async function checkEvalDenied({ extension, description, url, createTab }) {
+ info(`checkEvalDenied: ${description} (at URL: ${url})`);
+ await checkEvalResult({
+ extension,
+ description,
+ url,
+ createTab,
+ expectedResult: {
+ exceptionInfo: {
+ isError: true,
+ code: "E_PROTOCOLERROR",
+ details: [
+ "This extension is not allowed on the current inspected window origin",
+ ],
+ description: "Inspector protocol error: %s",
+ },
+ },
+ });
+}
+
+add_task(async function test_eval_at_http() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", false]],
+ });
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const httpUrl = "http://example.com/";
+
+ // When running with --use-http3-server, http:-URLs cannot be loaded.
+ try {
+ await fetch(httpUrl);
+ } catch {
+ info("Skipping test_eval_at_http because http:-URL cannot be loaded");
+ return;
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({});
+ await extension.startup();
+
+ await checkEvalAllowed({
+ extension,
+ description: "http:-URL",
+ url: httpUrl,
+ });
+ await extension.unload();
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_eval_at_https() {
+ const extension = ExtensionTestUtils.loadExtension({});
+ await extension.startup();
+
+ const privilegedExtension = ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+ });
+ await privilegedExtension.startup();
+
+ await checkEvalAllowed({
+ extension,
+ description: "https:-URL",
+ url: "https://example.com/",
+ });
+
+ await checkEvalDenied({
+ extension,
+ description: "a restricted domain",
+ // Domain in extensions.webextensions.restrictedDomains by browser.toml.
+ url: "https://test2.example.com/",
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.quarantinedDomains.list", "example.com"]],
+ });
+
+ await checkEvalDenied({
+ extension,
+ description: "a quarantined domain",
+ url: "https://example.com/",
+ });
+
+ await checkEvalAllowed({
+ extension: privilegedExtension,
+ description: "a quarantined domain",
+ url: "https://example.com/",
+ });
+
+ await SpecialPowers.popPrefEnv();
+
+ await extension.unload();
+ await privilegedExtension.unload();
+});
+
+add_task(async function test_eval_at_sandboxed_page() {
+ const extension = ExtensionTestUtils.loadExtension({});
+ await extension.startup();
+
+ await checkEvalAllowed({
+ extension,
+ description: "page with CSP sandbox",
+ url: "https://example.com/document-builder.sjs?headers=Content-Security-Policy:sandbox&html=x",
+ });
+ await checkEvalDenied({
+ extension,
+ description: "restricted domain with CSP sandbox",
+ url: "https://test2.example.com/document-builder.sjs?headers=Content-Security-Policy:sandbox&html=x",
+ });
+
+ await extension.unload();
+});
+
+add_task(async function test_eval_at_own_extension_origin_allowed() {
+ const extension = ExtensionTestUtils.loadExtension({
+ background() {
+ // eslint-disable-next-line no-undef
+ browser.test.sendMessage(
+ "blob_url",
+ URL.createObjectURL(new Blob(["blob: here", { type: "text/html" }]))
+ );
+ },
+ files: {
+ "mozext.html": `<!DOCTYPE html>moz-extension: here`,
+ },
+ });
+ await extension.startup();
+ const blobUrl = await extension.awaitMessage("blob_url");
+
+ await checkEvalAllowed({
+ extension,
+ description: "moz-extension:-URL from own extension",
+ url: `moz-extension://${extension.uuid}/mozext.html`,
+ });
+ await checkEvalAllowed({
+ extension,
+ description: "blob:-URL from own extension",
+ url: blobUrl,
+ });
+ await checkEvalAllowed({
+ extension,
+ description: "about:blank with origin from own extension",
+ url: "about:blank",
+ createTab: () => openAboutBlankTabWithExtensionOrigin(extension),
+ });
+
+ await extension.unload();
+});
+
+add_task(async function test_eval_at_other_extension_denied() {
+ // The extension for which we simulate devtools_page, chosen as caller of
+ // devtools.inspectedWindow.eval API calls.
+ const extension = ExtensionTestUtils.loadExtension({});
+ await extension.startup();
+
+ // The other extension, that |extension| should not be able to access:
+ const otherExt = ExtensionTestUtils.loadExtension({
+ background() {
+ // eslint-disable-next-line no-undef
+ browser.test.sendMessage(
+ "blob_url",
+ URL.createObjectURL(new Blob(["blob: here", { type: "text/html" }]))
+ );
+ },
+ files: {
+ "mozext.html": `<!DOCTYPE html>moz-extension: here`,
+ },
+ });
+ await otherExt.startup();
+ const otherExtBlobUrl = await otherExt.awaitMessage("blob_url");
+
+ await checkEvalDenied({
+ extension,
+ description: "moz-extension:-URL from another extension",
+ url: `moz-extension://${otherExt.uuid}/mozext.html`,
+ });
+ await checkEvalDenied({
+ extension,
+ description: "blob:-URL from another extension",
+ url: otherExtBlobUrl,
+ });
+ await checkEvalDenied({
+ extension,
+ description: "about:blank with origin from another extension",
+ url: "about:blank",
+ createTab: () => openAboutBlankTabWithExtensionOrigin(otherExt),
+ });
+
+ await otherExt.unload();
+ await extension.unload();
+});
+
+add_task(async function test_eval_at_about() {
+ const extension = ExtensionTestUtils.loadExtension({});
+ await extension.startup();
+ await checkEvalAllowed({
+ extension,
+ description: "about:blank (null principal)",
+ url: "about:blank",
+ });
+ await checkEvalDenied({
+ extension,
+ description: "about:addons (system principal)",
+ url: "about:addons",
+ });
+ await checkEvalDenied({
+ extension,
+ description: "about:robots (about page)",
+ url: "about:robots",
+ });
+ await extension.unload();
+});
+
+add_task(async function test_eval_at_file() {
+ // FYI: There is also an equivalent test case with a full end-to-end test at:
+ // browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_file.js
+
+ const extension = ExtensionTestUtils.loadExtension({});
+ await extension.startup();
+
+ // A dummy file URL that can be loaded in a tab.
+ const fileUrl =
+ "file://" +
+ getTestFilePath("browser_webextension_inspected_window_access.js");
+
+ // checkEvalAllowed test helper cannot be used, because the file:-URL may
+ // redirect elsewhere, so the comparison with the full URL fails.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, fileUrl);
+ const result = await run_inspectedWindow_eval({
+ tab,
+ codeToEval: "'code executed at ' + location.protocol",
+ extension,
+ });
+ BrowserTestUtils.removeTab(tab);
+ SimpleTest.isDeeply(
+ result,
+ { value: "code executed at file:" },
+ `eval result for devtools.inspectedWindow.eval at ${fileUrl}`
+ );
+
+ await extension.unload();
+});
diff --git a/devtools/shared/commands/inspected-window/tests/head.js b/devtools/shared/commands/inspected-window/tests/head.js
new file mode 100644
index 0000000000..ce65b3d827
--- /dev/null
+++ b/devtools/shared/commands/inspected-window/tests/head.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
diff --git a/devtools/shared/commands/inspected-window/tests/inspectedwindow-reload-target.sjs b/devtools/shared/commands/inspected-window/tests/inspectedwindow-reload-target.sjs
new file mode 100644
index 0000000000..2ec17a9222
--- /dev/null
+++ b/devtools/shared/commands/inspected-window/tests/inspectedwindow-reload-target.sjs
@@ -0,0 +1,89 @@
+"use strict";
+
+Cu.importGlobalProperties(["URLSearchParams"]);
+
+function handleRequest(request, response) {
+ const params = new URLSearchParams(request.queryString);
+
+ switch (params.get("test")) {
+ case "cache":
+ handleCacheTestRequest(request, response);
+ break;
+
+ case "user-agent":
+ handleUserAgentTestRequest(request, response);
+ break;
+
+ case "injected-script":
+ handleInjectedScriptTestRequest(request, response, params);
+ break;
+ }
+}
+
+function handleCacheTestRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain; charset=UTF-8", false);
+
+ if (request.hasHeader("pragma") && request.hasHeader("cache-control")) {
+ response.write(
+ `${request.getHeader("pragma")}:${request.getHeader("cache-control")}`
+ );
+ } else {
+ response.write("empty cache headers");
+ }
+}
+
+function handleUserAgentTestRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain; charset=UTF-8", false);
+
+ if (request.hasHeader("user-agent")) {
+ response.write(request.getHeader("user-agent"));
+ } else {
+ response.write("no user agent header");
+ }
+}
+
+function handleInjectedScriptTestRequest(request, response, params) {
+ response.setHeader("Content-Type", "text/html; charset=UTF-8", false);
+
+ const frames = parseInt(params.get("frames"), 10);
+ let content = "";
+
+ if (frames > 0) {
+ // Output an iframe in seamless mode, so that there is an higher chance that in case
+ // of test failures we get a screenshot where the nested iframes are all visible.
+ content = `<iframe seamless src="?test=injected-script&frames=${
+ frames - 1
+ }"></iframe>`;
+ } else {
+ // Output an about:srcdoc frame to be sure that inspectedWindow.eval is able to
+ // evaluate js code into it.
+ const srcdoc = `
+ <pre>injected script NOT executed</pre>
+ <script>window.pageScriptExecutedFirst = true</script>
+ `;
+ content = `<iframe style="height: 30px;" srcdoc="${srcdoc}"></iframe>`;
+ }
+
+ if (params.get("stop") == "windowStop") {
+ content = "<script>window.stop();</script>" + content;
+ }
+
+ response.write(`<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ iframe { width: 100%; height: ${frames * 150}px; }
+ </style>
+ </head>
+ <body>
+ <h1>IFRAME ${frames}</h1>
+ <pre>injected script NOT executed</pre>
+ <script>
+ window.pageScriptExecutedFirst = true;
+ </script>
+ ${content}
+ </body>
+ </html>
+ `);
+}