summaryrefslogtreecommitdiffstats
path: root/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js')
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js1348
1 files changed, 1348 insertions, 0 deletions
diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js
new file mode 100644
index 0000000000..7d51a9c59a
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js
@@ -0,0 +1,1348 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+
+const ROOT_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ ""
+);
+
+// Testing multiple protocol / origin combinations takes long on debug.
+requestLongerTimeout(7);
+
+const DIALOG_URL_APP_CHOOSER =
+ "chrome://mozapps/content/handling/appChooser.xhtml";
+const DIALOG_URL_PERMISSION =
+ "chrome://mozapps/content/handling/permissionDialog.xhtml";
+
+const PROTOCOL_HANDLER_OPEN_PERM_KEY = "open-protocol-handler";
+const PERMISSION_KEY_DELIMITER = "^";
+
+const TEST_PROTOS = ["foo", "bar"];
+
+let testDir = getChromeDir(getResolvedURI(gTestPath));
+
+const ORIGIN1 = "https://example.com";
+const ORIGIN2 = "https://example.org";
+const ORIGIN3 = Services.io.newFileURI(testDir).spec;
+const PRINCIPAL1 =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(ORIGIN1);
+const PRINCIPAL2 =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(ORIGIN2);
+const PRINCIPAL3 =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(ORIGIN3);
+
+const NULL_PRINCIPAL_SCHEME = Services.scriptSecurityManager
+ .createNullPrincipal({})
+ .scheme.toLowerCase();
+
+/**
+ * Get the open protocol handler permission key for a given protocol scheme.
+ * @param {string} aProtocolScheme - Scheme of protocol to construct permission
+ * key with.
+ */
+function getSkipProtoDialogPermissionKey(aProtocolScheme) {
+ return (
+ PROTOCOL_HANDLER_OPEN_PERM_KEY + PERMISSION_KEY_DELIMITER + aProtocolScheme
+ );
+}
+
+/**
+ * Creates dummy web protocol handlers used for testing.
+ */
+function initTestHandlers() {
+ TEST_PROTOS.forEach(scheme => {
+ let webHandler = Cc[
+ "@mozilla.org/uriloader/web-handler-app;1"
+ ].createInstance(Ci.nsIWebHandlerApp);
+ webHandler.name = scheme + "Handler";
+ webHandler.uriTemplate = ORIGIN1 + "/?url=%s";
+
+ let handlerInfo = HandlerServiceTestUtils.getBlankHandlerInfo(scheme);
+ handlerInfo.possibleApplicationHandlers.appendElement(webHandler);
+ handlerInfo.preferredApplicationHandler = webHandler;
+ gHandlerService.store(handlerInfo);
+ });
+}
+
+/**
+ * Update whether the protocol handler dialog is shown for our test protocol +
+ * handler.
+ * @param {string} scheme - Scheme of the protocol to change the ask state for.
+ * @param {boolean} ask - true => show dialog, false => skip dialog.
+ */
+function updateAlwaysAsk(scheme, ask) {
+ let handlerInfo = HandlerServiceTestUtils.getHandlerInfo(scheme);
+ handlerInfo.alwaysAskBeforeHandling = ask;
+ gHandlerService.store(handlerInfo);
+}
+
+/**
+ * Test whether the protocol handler dialog is set to show for our
+ * test protocol + handler.
+ * @param {string} scheme - Scheme of the protocol to test the ask state for.
+ * @param {boolean} ask - true => show dialog, false => skip dialog.
+ */
+function testAlwaysAsk(scheme, ask) {
+ is(
+ HandlerServiceTestUtils.getHandlerInfo(scheme).alwaysAskBeforeHandling,
+ ask,
+ "Should have correct alwaysAsk state."
+ );
+}
+
+/**
+ * Triggers the load via a server redirect.
+ * @param {string} serverRedirect - The redirect type.
+ */
+function useServerRedirect(serverRedirect) {
+ return async (browser, scheme) => {
+ let uri = `${scheme}://test`;
+
+ let innerParams = new URLSearchParams();
+ innerParams.set("uri", uri);
+ innerParams.set("redirectType", serverRedirect);
+ let params = new URLSearchParams();
+ params.set(
+ "uri",
+ "https://example.com/" +
+ ROOT_PATH +
+ "redirect_helper.sjs?" +
+ innerParams.toString()
+ );
+ uri =
+ "https://example.org/" +
+ ROOT_PATH +
+ "redirect_helper.sjs?" +
+ params.toString();
+ BrowserTestUtils.loadURIString(browser, uri);
+ };
+}
+
+/**
+ * Triggers the load with a specific principal or the browser's current
+ * principal.
+ * @param {nsIPrincipal} [principal] - Principal to use to trigger the load.
+ */
+function useTriggeringPrincipal(principal = undefined) {
+ return async (browser, scheme) => {
+ let uri = `${scheme}://test`;
+ let triggeringPrincipal = principal ?? browser.contentPrincipal;
+
+ info("Loading uri: " + uri);
+ browser.loadURI(Services.io.newURI(uri), { triggeringPrincipal });
+ };
+}
+
+/**
+ * Navigates to a test URL with the given protocol scheme and waits for the
+ * result.
+ * @param {MozBrowser} browser - Browser to navigate.
+ * @param {string} scheme - Scheme of the test url. e.g. irc
+ * @param {Object} [options] - Test options.
+ * @param {Object} [options.permDialogOptions] - Test options for the permission
+ * dialog. If defined, we expect this dialog to be shown.
+ * @param {Object} [options.chooserDialogOptions] - Test options for the chooser
+ * dialog. If defined, we expect this dialog to be shown.
+ * @param {Function} [options.triggerLoad] - An async callback function to
+ * trigger the load. Will be passed the browser and scheme to use.
+ * @param {nsIPrincipal} [options.triggeringPrincipal] - Principal to trigger
+ * the load with. Defaults to the browsers content principal.
+ * @returns {Promise} - A promise which resolves once the test is complete.
+ */
+async function testOpenProto(
+ browser,
+ scheme,
+ {
+ permDialogOptions,
+ chooserDialogOptions,
+ triggerLoad = useTriggeringPrincipal(),
+ } = {}
+) {
+ let permDialogOpenPromise;
+ let chooserDialogOpenPromise;
+
+ if (permDialogOptions) {
+ info("Should see permission dialog");
+ permDialogOpenPromise = waitForProtocolPermissionDialog(browser, true);
+ }
+
+ if (chooserDialogOptions) {
+ info("Should see chooser dialog");
+ chooserDialogOpenPromise = waitForProtocolAppChooserDialog(browser, true);
+ }
+ await triggerLoad(browser, scheme);
+ let webHandlerLoadedPromise;
+
+ let webHandlerShouldOpen =
+ (!permDialogOptions && !chooserDialogOptions) ||
+ ((permDialogOptions?.actionConfirm || permDialogOptions?.actionChangeApp) &&
+ chooserDialogOptions?.actionConfirm);
+
+ // Register web handler load listener if we expect to trigger it.
+ if (webHandlerShouldOpen) {
+ webHandlerLoadedPromise = waitForHandlerURL(browser, scheme);
+ }
+
+ if (permDialogOpenPromise) {
+ let dialog = await permDialogOpenPromise;
+ let dialogEl = getDialogElementFromSubDialog(dialog);
+ let dialogType = getDialogType(dialog);
+
+ let {
+ hasCheckbox,
+ checkboxOrigin,
+ hasChangeApp,
+ chooserIsNext,
+ actionCheckbox,
+ actionConfirm,
+ actionChangeApp,
+ checkContents,
+ } = permDialogOptions;
+
+ if (actionChangeApp) {
+ actionConfirm = false;
+ }
+
+ let descriptionEl = dialogEl.querySelector("#description");
+ ok(
+ descriptionEl && BrowserTestUtils.is_visible(descriptionEl),
+ "Has a visible description element."
+ );
+
+ ok(
+ !descriptionEl.innerHTML.toLowerCase().includes(NULL_PRINCIPAL_SCHEME),
+ "Description does not include NullPrincipal scheme."
+ );
+
+ await testCheckbox(dialogEl, dialogType, {
+ hasCheckbox,
+ actionCheckbox,
+ checkboxOrigin,
+ });
+
+ // Check the button label depending on whether we would show the chooser
+ // dialog next or directly open the handler.
+ let acceptBtnLabel = dialogEl.getButton("accept")?.label;
+
+ if (chooserIsNext) {
+ is(
+ acceptBtnLabel,
+ "Choose Application",
+ "Accept button has choose app label"
+ );
+ } else {
+ is(acceptBtnLabel, "Open Link", "Accept button has open link label");
+ }
+
+ let changeAppLink = dialogEl.ownerDocument.getElementById("change-app");
+ if (typeof hasChangeApp == "boolean") {
+ ok(changeAppLink, "Permission dialog should have changeApp link label");
+ is(
+ !changeAppLink.hidden,
+ hasChangeApp,
+ "Permission dialog change app link label"
+ );
+ }
+
+ if (checkContents) {
+ checkContents(dialogEl);
+ }
+
+ if (actionChangeApp) {
+ let dialogClosedPromise = waitForProtocolPermissionDialog(browser, false);
+ changeAppLink.click();
+ await dialogClosedPromise;
+ } else {
+ await closeDialog(browser, dialog, actionConfirm, scheme);
+ }
+ }
+
+ if (chooserDialogOpenPromise) {
+ let dialog = await chooserDialogOpenPromise;
+ let dialogEl = getDialogElementFromSubDialog(dialog);
+ let dialogType = getDialogType(dialog);
+
+ let { hasCheckbox, actionCheckbox, actionConfirm } = chooserDialogOptions;
+
+ await testCheckbox(dialogEl, dialogType, {
+ hasCheckbox,
+ actionCheckbox,
+ });
+
+ await closeDialog(browser, dialog, actionConfirm, scheme);
+ }
+
+ if (webHandlerShouldOpen) {
+ info("Waiting for web handler to open");
+ await webHandlerLoadedPromise;
+ } else {
+ info("Web handler open canceled");
+ }
+}
+
+/**
+ * Inspects the checkbox state and interacts with it.
+ * @param {dialog} dialogEl
+ * @param {string} dialogType - String identifier of dialog type.
+ * Either "permission" or "chooser".
+ * @param {Object} options - Test Options.
+ * @param {boolean} [options.hasCheckbox] - Whether the dialog is expected to
+ * have a visible checkbox.
+ * @param {boolean} [options.hasCheckboxState] - The check state of the checkbox
+ * to test for. true = checked, false = unchecked.
+ * @param {boolean} [options.actionCheckbox] - The state to set on the checkbox.
+ * true = checked, false = unchecked.
+ */
+async function testCheckbox(
+ dialogEl,
+ dialogType,
+ { hasCheckbox, hasCheckboxState = false, actionCheckbox, checkboxOrigin }
+) {
+ let checkbox = dialogEl.ownerDocument.getElementById("remember");
+ if (typeof hasCheckbox == "boolean") {
+ is(
+ checkbox && BrowserTestUtils.is_visible(checkbox),
+ hasCheckbox,
+ "Dialog checkbox has correct visibility."
+ );
+
+ let checkboxLabel = dialogEl.ownerDocument.getElementById("remember-label");
+ is(
+ checkbox && BrowserTestUtils.is_visible(checkboxLabel),
+ hasCheckbox,
+ "Dialog checkbox label has correct visibility."
+ );
+ if (hasCheckbox) {
+ ok(
+ !checkboxLabel.innerHTML.toLowerCase().includes(NULL_PRINCIPAL_SCHEME),
+ "Dialog checkbox label does not include NullPrincipal scheme."
+ );
+ }
+ }
+
+ if (typeof hasCheckboxState == "boolean") {
+ is(checkbox.checked, hasCheckboxState, "Dialog checkbox has correct state");
+ }
+
+ if (checkboxOrigin) {
+ let doc = dialogEl.ownerDocument;
+ let hostFromLabel = doc.l10n.getAttributes(
+ doc.getElementById("remember-label")
+ ).args.host;
+ is(hostFromLabel, checkboxOrigin, "Checkbox should be for correct domain.");
+ }
+
+ if (typeof actionCheckbox == "boolean") {
+ checkbox.click();
+ }
+}
+
+/**
+ * Get the dialog element which is a child of the SubDialogs browser frame.
+ * @param {SubDialog} subDialog - Dialog to get the dialog element for.
+ */
+function getDialogElementFromSubDialog(subDialog) {
+ let dialogEl = subDialog._frame.contentDocument.querySelector("dialog");
+ ok(dialogEl, "SubDialog should have dialog element");
+ return dialogEl;
+}
+
+/**
+ * Wait for the test handler to be opened.
+ * @param {MozBrowser} browser - The browser the load should occur in.
+ * @param {string} scheme - Scheme which triggered the handler to open.
+ */
+function waitForHandlerURL(browser, scheme) {
+ return BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ url => url == `${ORIGIN1}/?url=${scheme}%3A%2F%2Ftest`
+ );
+}
+
+/**
+ * Test for open-protocol-handler permission.
+ * @param {nsIPrincipal} principal - The principal to test the permission on.
+ * @param {string} scheme - Scheme to generate permission key.
+ * @param {boolean} hasPerm - Whether we expect the princial to set the
+ * permission (true), or not (false).
+ */
+function testPermission(principal, scheme, hasPerm) {
+ let permKey = getSkipProtoDialogPermissionKey(scheme);
+ let result = Services.perms.testPermissionFromPrincipal(principal, permKey);
+ let message = `${permKey} ${hasPerm ? "is" : "is not"} set for ${
+ principal.origin
+ }.`;
+ is(result == Services.perms.ALLOW_ACTION, hasPerm, message);
+}
+
+/**
+ * Get the checkbox element of the dialog used to remember the handler choice or
+ * store the permission.
+ * @param {SubDialog} dialog - Protocol handler dialog embedded in a SubDialog.
+ * @param {string} dialogType - Type of the dialog which holds the checkbox.
+ * @returns {HTMLInputElement} - Checkbox of the dialog.
+ */
+function getDialogCheckbox(dialog, dialogType) {
+ let id;
+ if (dialogType == "permission") {
+ id = "remember-permission";
+ } else {
+ id = "remember";
+ }
+ return dialog._frame.contentDocument.getElementById(id);
+}
+
+function getDialogType(dialog) {
+ let url = dialog._frame.currentURI.spec;
+
+ if (url === DIALOG_URL_PERMISSION) {
+ return "permission";
+ }
+ if (url === DIALOG_URL_APP_CHOOSER) {
+ return "chooser";
+ }
+ throw new Error("Dialog with unexpected url");
+}
+
+/**
+ * Exit a protocol handler SubDialog and wait for it to be fully closed.
+ * @param {MozBrowser} browser - Browser element of the tab where the dialog is
+ * shown.
+ * @param {SubDialog} dialog - SubDialog object which holds the protocol handler
+ * @param {boolean} confirm - Whether to confirm (true) or cancel (false) the
+ * dialog.
+ * @param {string} scheme - The scheme of the protocol the dialog is opened for.
+ * dialog.
+ */
+async function closeDialog(browser, dialog, confirm, scheme) {
+ let dialogClosedPromise = waitForSubDialog(browser, dialog._openedURL, false);
+ let dialogEl = getDialogElementFromSubDialog(dialog);
+
+ if (confirm) {
+ if (getDialogType(dialog) == "chooser") {
+ // Select our test protocol handler
+ let listItem = dialogEl.ownerDocument.querySelector(
+ `richlistitem[name="${scheme}Handler"]`
+ );
+ listItem.click();
+ }
+
+ dialogEl.setAttribute("buttondisabledaccept", false);
+ dialogEl.acceptDialog();
+ } else {
+ dialogEl.cancelDialog();
+ }
+
+ return dialogClosedPromise;
+}
+
+registerCleanupFunction(function () {
+ // Clean up test handlers
+ TEST_PROTOS.forEach(scheme => {
+ let handlerInfo = HandlerServiceTestUtils.getHandlerInfo(scheme);
+ gHandlerService.remove(handlerInfo);
+ });
+
+ // Clear permissions
+ Services.perms.removeAll();
+});
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.external_protocol_requires_permission", true]],
+ });
+ initTestHandlers();
+});
+
+/**
+ * Tests that when "remember" is unchecked, we only allow the protocol to be
+ * opened once and don't store any permission.
+ */
+add_task(async function test_permission_allow_once() {
+ for (let scheme of TEST_PROTOS) {
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: true,
+ hasChangeApp: false,
+ chooserIsNext: true,
+ actionConfirm: true,
+ },
+ chooserDialogOptions: { hasCheckbox: true, actionConfirm: true },
+ });
+ });
+
+ // No permission should be set
+ testPermission(PRINCIPAL1, scheme, false);
+ testPermission(PRINCIPAL2, scheme, false);
+
+ // No preferred app should be set
+ testAlwaysAsk(scheme, true);
+
+ // If we open again we should see the permission dialog
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: true,
+ hasChangeApp: false,
+ chooserIsNext: true,
+ actionConfirm: false,
+ },
+ });
+ });
+ }
+});
+
+/**
+ * Tests that when checking the "remember" checkbox, the protocol permission
+ * is set correctly and allows the caller to skip the permission dialog in
+ * subsequent calls.
+ */
+add_task(async function test_permission_allow_persist() {
+ for (let [origin, principal] of [
+ [ORIGIN1, PRINCIPAL1],
+ [ORIGIN3, PRINCIPAL3],
+ ]) {
+ for (let scheme of TEST_PROTOS) {
+ info("Testing with origin " + origin);
+ info("testing with principal of origin " + principal.origin);
+ info("testing with protocol " + scheme);
+
+ // Set a permission for an unrelated protocol.
+ // We should still see the permission dialog.
+ Services.perms.addFromPrincipal(
+ principal,
+ getSkipProtoDialogPermissionKey("foobar"),
+ Services.perms.ALLOW_ACTION
+ );
+
+ await BrowserTestUtils.withNewTab(origin, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: true,
+ hasChangeApp: false,
+ chooserIsNext: true,
+ actionCheckbox: true,
+ actionConfirm: true,
+ },
+ chooserDialogOptions: { hasCheckbox: true, actionConfirm: true },
+ });
+ });
+
+ // Permission should be set
+ testPermission(principal, scheme, true);
+ testPermission(PRINCIPAL2, scheme, false);
+
+ // No preferred app should be set
+ testAlwaysAsk(scheme, true);
+
+ // If we open again with the origin where we granted permission, we should
+ // directly get the chooser dialog.
+ await BrowserTestUtils.withNewTab(origin, async browser => {
+ await testOpenProto(browser, scheme, {
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false,
+ },
+ });
+ });
+
+ // If we open with the other origin, we should see the permission dialog
+ await BrowserTestUtils.withNewTab(ORIGIN2, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: true,
+ hasChangeApp: false,
+ chooserIsNext: true,
+ actionConfirm: false,
+ },
+ });
+ });
+
+ // Cleanup permissions
+ Services.perms.removeAll();
+ }
+ }
+});
+
+/**
+ * Tests that if a preferred protocol handler is set, the permission dialog
+ * shows the application name and a link which leads to the app chooser.
+ */
+add_task(async function test_permission_application_set() {
+ let scheme = TEST_PROTOS[0];
+ updateAlwaysAsk(scheme, false);
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: true,
+ hasChangeApp: true,
+ chooserIsNext: false,
+ actionChangeApp: true,
+ },
+ chooserDialogOptions: { hasCheckbox: true, actionConfirm: true },
+ });
+ });
+
+ // Cleanup
+ updateAlwaysAsk(scheme, true);
+});
+
+/**
+ * Tests that we correctly handle system principals. They should always
+ * skip the permission dialog.
+ */
+add_task(async function test_permission_system_principal() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ chooserDialogOptions: { hasCheckbox: true, actionConfirm: false },
+ triggerLoad: useTriggeringPrincipal(
+ Services.scriptSecurityManager.getSystemPrincipal()
+ ),
+ });
+ });
+});
+
+/**
+ * Tests that we don't show the permission dialog if the permission is disabled
+ * by pref.
+ */
+add_task(async function test_permission_disabled() {
+ let scheme = TEST_PROTOS[0];
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.external_protocol_requires_permission", false]],
+ });
+
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ chooserDialogOptions: { hasCheckbox: true, actionConfirm: true },
+ });
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Tests that we directly open the handler if permission and handler are set.
+ */
+add_task(async function test_app_and_permission_set() {
+ let scheme = TEST_PROTOS[1];
+
+ updateAlwaysAsk(scheme, false);
+ Services.perms.addFromPrincipal(
+ PRINCIPAL2,
+ getSkipProtoDialogPermissionKey(scheme),
+ Services.perms.ALLOW_ACTION
+ );
+
+ await BrowserTestUtils.withNewTab(ORIGIN2, async browser => {
+ await testOpenProto(browser, scheme);
+ });
+
+ // Cleanup
+ Services.perms.removeAll();
+ updateAlwaysAsk(scheme, true);
+});
+
+/**
+ * Tests that the alwaysAsk state is not updated if the user cancels the dialog
+ */
+add_task(async function test_change_app_checkbox_cancel() {
+ let scheme = TEST_PROTOS[0];
+
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: true,
+ chooserIsNext: true,
+ hasChangeApp: false,
+ actionConfirm: true,
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionCheckbox: true, // Activate checkbox
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+
+ // Should not have applied value from checkbox
+ testAlwaysAsk(scheme, true);
+});
+
+/**
+ * Tests that the external protocol dialogs behave correctly when a null
+ * principal is passed.
+ */
+add_task(async function test_null_principal() {
+ let scheme = TEST_PROTOS[0];
+
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ triggerLoad: () => {
+ let uri = `${scheme}://test`;
+ ContentTask.spawn(browser, { uri }, args => {
+ let frame = content.document.createElement("iframe");
+ frame.src = `data:text/html,<script>location.href="${args.uri}"</script>`;
+ content.document.body.appendChild(frame);
+ });
+ },
+ permDialogOptions: {
+ hasCheckbox: false,
+ chooserIsNext: true,
+ hasChangeApp: false,
+ actionConfirm: true,
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
+
+/**
+ * Tests that the external protocol dialogs behave correctly when no principal
+ * is passed.
+ */
+add_task(async function test_no_principal() {
+ let scheme = TEST_PROTOS[1];
+
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ triggerLoad: () => {
+ let uri = `${scheme}://test`;
+
+ let contentDispatchChooser = Cc[
+ "@mozilla.org/content-dispatch-chooser;1"
+ ].createInstance(Ci.nsIContentDispatchChooser);
+
+ let handler = HandlerServiceTestUtils.getHandlerInfo(scheme);
+
+ contentDispatchChooser.handleURI(
+ handler,
+ Services.io.newURI(uri),
+ null,
+ browser.browsingContext
+ );
+ },
+ permDialogOptions: {
+ hasCheckbox: false,
+ chooserIsNext: true,
+ hasChangeApp: false,
+ actionConfirm: true,
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
+
+/**
+ * Tests that if a URI scheme has a non-standard protocol, an OS default exists,
+ * and the user hasn't selected an alternative only the permission dialog is shown.
+ */
+add_task(async function test_non_standard_protocol() {
+ let scheme = null;
+ // TODO add a scheme for Windows 10 or greater once support is added (see bug 1764599).
+ if (AppConstants.platform == "macosx") {
+ scheme = "itunes";
+ } else {
+ info(
+ "Skipping this test since there isn't a suitable default protocol on this platform"
+ );
+ return;
+ }
+
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: true,
+ hasChangeApp: true,
+ chooserIsNext: false,
+ actionChangeApp: false,
+ },
+ });
+ });
+});
+
+/**
+ * Tests that we show the permission dialog for extension content scripts.
+ */
+add_task(async function test_extension_content_script_permission() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ let testExtension;
+
+ await testOpenProto(browser, scheme, {
+ triggerLoad: async () => {
+ let uri = `${scheme}://test`;
+
+ const EXTENSION_DATA = {
+ manifest: {
+ content_scripts: [
+ {
+ matches: [browser.currentURI.spec],
+ js: ["navigate.js"],
+ },
+ ],
+ browser_specific_settings: {
+ gecko: { id: "allowed@mochi.test" },
+ },
+ },
+ files: {
+ "navigate.js": `window.location.href = "${uri}";`,
+ },
+ useAddonManager: "permanent",
+ };
+
+ testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA);
+ await testExtension.startup();
+ },
+ permDialogOptions: {
+ hasCheckbox: true,
+ chooserIsNext: true,
+ hasChangeApp: false,
+ actionCheckbox: true,
+ actionConfirm: true,
+ checkContents: dialogEl => {
+ let description = dialogEl.querySelector("#description");
+ let { id, args } =
+ description.ownerDocument.l10n.getAttributes(description);
+ is(
+ id,
+ "permission-dialog-description-extension",
+ "Should be using the correct string."
+ );
+ is(
+ args.extension,
+ "Generated extension",
+ "Should have the correct extension name."
+ );
+ },
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+
+ let extensionPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(`moz-extension://${testExtension.uuid}/`),
+ {}
+ );
+ let extensionPrivatePrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(`moz-extension://${testExtension.uuid}/`),
+ { privateBrowsingId: 1 }
+ );
+
+ let key = getSkipProtoDialogPermissionKey(scheme);
+ is(
+ Services.perms.testPermissionFromPrincipal(extensionPrincipal, key),
+ Services.perms.ALLOW_ACTION,
+ "Should have permanently allowed the extension"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ extensionPrivatePrincipal,
+ key
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "Should not have changed the private principal permission"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(PRINCIPAL1, key),
+ Services.perms.UNKNOWN_ACTION,
+ "Should not have allowed the page"
+ );
+
+ await testExtension.unload();
+
+ is(
+ Services.perms.testPermissionFromPrincipal(extensionPrincipal, key),
+ Services.perms.UNKNOWN_ACTION,
+ "Should have cleared the extension's normal principal permission"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ extensionPrivatePrincipal,
+ key
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "Should have cleared the private browsing principal"
+ );
+ });
+});
+
+/**
+ * Tests that we show the permission dialog for extension content scripts.
+ */
+add_task(async function test_extension_private_content_script_permission() {
+ let scheme = TEST_PROTOS[0];
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: ORIGIN1 },
+ async browser => {
+ let testExtension;
+
+ await testOpenProto(browser, scheme, {
+ triggerLoad: async () => {
+ let uri = `${scheme}://test`;
+
+ const EXTENSION_DATA = {
+ manifest: {
+ content_scripts: [
+ {
+ matches: [browser.currentURI.spec],
+ js: ["navigate.js"],
+ },
+ ],
+ browser_specific_settings: {
+ gecko: { id: "allowed@mochi.test" },
+ },
+ },
+ files: {
+ "navigate.js": `window.location.href = "${uri}";`,
+ },
+ useAddonManager: "permanent",
+ };
+
+ testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA);
+ await testExtension.startup();
+ let perms = {
+ permissions: ["internal:privateBrowsingAllowed"],
+ origins: [],
+ };
+ await ExtensionPermissions.add("allowed@mochi.test", perms);
+ let addon = await AddonManager.getAddonByID("allowed@mochi.test");
+ await addon.reload();
+ },
+ permDialogOptions: {
+ hasCheckbox: true,
+ chooserIsNext: true,
+ hasChangeApp: false,
+ actionCheckbox: true,
+ actionConfirm: true,
+ checkContents: dialogEl => {
+ let description = dialogEl.querySelector("#description");
+ let { id, args } =
+ description.ownerDocument.l10n.getAttributes(description);
+ is(
+ id,
+ "permission-dialog-description-extension",
+ "Should be using the correct string."
+ );
+ is(
+ args.extension,
+ "Generated extension",
+ "Should have the correct extension name."
+ );
+ },
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+
+ let extensionPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(`moz-extension://${testExtension.uuid}/`),
+ {}
+ );
+ let extensionPrivatePrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(`moz-extension://${testExtension.uuid}/`),
+ { privateBrowsingId: 1 }
+ );
+
+ let key = getSkipProtoDialogPermissionKey(scheme);
+ is(
+ Services.perms.testPermissionFromPrincipal(extensionPrincipal, key),
+ Services.perms.UNKNOWN_ACTION,
+ "Should not have changed the extension's normal principal permission"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ extensionPrivatePrincipal,
+ key
+ ),
+ Services.perms.ALLOW_ACTION,
+ "Should have allowed the private browsing principal"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(PRINCIPAL1, key),
+ Services.perms.UNKNOWN_ACTION,
+ "Should not have allowed the page"
+ );
+
+ await testExtension.unload();
+
+ is(
+ Services.perms.testPermissionFromPrincipal(extensionPrincipal, key),
+ Services.perms.UNKNOWN_ACTION,
+ "Should have cleared the extension's normal principal permission"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ extensionPrivatePrincipal,
+ key
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "Should have cleared the private browsing principal"
+ );
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests that we do not show the permission dialog for extension content scripts
+ * when the page already has permission.
+ */
+add_task(async function test_extension_allowed_content() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ let testExtension;
+
+ let key = getSkipProtoDialogPermissionKey(scheme);
+ Services.perms.addFromPrincipal(
+ PRINCIPAL1,
+ key,
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_NEVER
+ );
+
+ await testOpenProto(browser, scheme, {
+ triggerLoad: async () => {
+ let uri = `${scheme}://test`;
+
+ const EXTENSION_DATA = {
+ manifest: {
+ content_scripts: [
+ {
+ matches: [browser.currentURI.spec],
+ js: ["navigate.js"],
+ },
+ ],
+ },
+ files: {
+ "navigate.js": `window.location.href = "${uri}";`,
+ },
+ };
+
+ testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA);
+ await testExtension.startup();
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+
+ let extensionPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(`moz-extension://${testExtension.uuid}/`),
+ {}
+ );
+
+ is(
+ Services.perms.testPermissionFromPrincipal(extensionPrincipal, key),
+ Services.perms.UNKNOWN_ACTION,
+ "Should not have permanently allowed the extension"
+ );
+
+ await testExtension.unload();
+ Services.perms.removeFromPrincipal(PRINCIPAL1, key);
+ });
+});
+
+/**
+ * Tests that we do not show the permission dialog for extension content scripts
+ * when the extension already has permission.
+ */
+add_task(async function test_extension_allowed_extension() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ let testExtension;
+
+ let key = getSkipProtoDialogPermissionKey(scheme);
+
+ await testOpenProto(browser, scheme, {
+ triggerLoad: async () => {
+ const EXTENSION_DATA = {
+ manifest: {
+ permissions: [`${ORIGIN1}/*`],
+ },
+ background() {
+ browser.test.onMessage.addListener(async (msg, uri) => {
+ switch (msg) {
+ case "engage":
+ browser.tabs.executeScript({
+ code: `window.location.href = "${uri}";`,
+ });
+ break;
+ default:
+ browser.test.fail(`Unexpected message received: ${msg}`);
+ }
+ });
+ },
+ };
+
+ testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA);
+ await testExtension.startup();
+
+ let extensionPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(`moz-extension://${testExtension.uuid}/`),
+ {}
+ );
+ Services.perms.addFromPrincipal(
+ extensionPrincipal,
+ key,
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_NEVER
+ );
+
+ testExtension.sendMessage("engage", `${scheme}://test`);
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+
+ await testExtension.unload();
+ Services.perms.removeFromPrincipal(PRINCIPAL1, key);
+ });
+});
+
+/**
+ * Tests that we show the permission dialog for extensions directly opening a
+ * protocol.
+ */
+add_task(async function test_extension_principal() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ let testExtension;
+
+ await testOpenProto(browser, scheme, {
+ triggerLoad: async () => {
+ const EXTENSION_DATA = {
+ background() {
+ browser.test.onMessage.addListener(async (msg, url) => {
+ switch (msg) {
+ case "engage":
+ browser.tabs.update({
+ url,
+ });
+ break;
+ default:
+ browser.test.fail(`Unexpected message received: ${msg}`);
+ }
+ });
+ },
+ };
+
+ testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA);
+ await testExtension.startup();
+ testExtension.sendMessage("engage", `${scheme}://test`);
+ },
+ permDialogOptions: {
+ hasCheckbox: true,
+ chooserIsNext: true,
+ hasChangeApp: false,
+ actionCheckbox: true,
+ actionConfirm: true,
+ checkContents: dialogEl => {
+ let description = dialogEl.querySelector("#description");
+ let { id, args } =
+ description.ownerDocument.l10n.getAttributes(description);
+ is(
+ id,
+ "permission-dialog-description-extension",
+ "Should be using the correct string."
+ );
+ is(
+ args.extension,
+ "Generated extension",
+ "Should have the correct extension name."
+ );
+ },
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+
+ let extensionPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(`moz-extension://${testExtension.uuid}/`),
+ {}
+ );
+
+ let key = getSkipProtoDialogPermissionKey(scheme);
+ is(
+ Services.perms.testPermissionFromPrincipal(extensionPrincipal, key),
+ Services.perms.ALLOW_ACTION,
+ "Should have permanently allowed the extension"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(PRINCIPAL1, key),
+ Services.perms.UNKNOWN_ACTION,
+ "Should not have allowed the page"
+ );
+
+ await testExtension.unload();
+ });
+});
+
+/**
+ * Test that we use the redirect principal for the dialog when applicable.
+ */
+add_task(async function test_redirect_principal() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ await testOpenProto(browser, scheme, {
+ triggerLoad: useServerRedirect("location"),
+ permDialogOptions: {
+ checkboxOrigin: ORIGIN1,
+ chooserIsNext: true,
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
+
+/**
+ * Test that we use the redirect principal for the dialog for refresh headers.
+ */
+add_task(async function test_redirect_principal() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ await testOpenProto(browser, scheme, {
+ triggerLoad: useServerRedirect("refresh"),
+ permDialogOptions: {
+ checkboxOrigin: ORIGIN1,
+ chooserIsNext: true,
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
+
+/**
+ * Test that we use the redirect principal for the dialog for meta refreshes.
+ */
+add_task(async function test_redirect_principal() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ await testOpenProto(browser, scheme, {
+ triggerLoad: useServerRedirect("meta-refresh"),
+ permDialogOptions: {
+ checkboxOrigin: ORIGIN1,
+ chooserIsNext: true,
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
+
+/**
+ * Test that we use the redirect principal for the dialog for JS redirects.
+ */
+add_task(async function test_redirect_principal_js() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ await testOpenProto(browser, scheme, {
+ triggerLoad: () => {
+ let uri = `${scheme}://test`;
+
+ let innerParams = new URLSearchParams();
+ innerParams.set("uri", uri);
+ let params = new URLSearchParams();
+ params.set(
+ "uri",
+ "https://example.com/" +
+ ROOT_PATH +
+ "script_redirect.html?" +
+ innerParams.toString()
+ );
+ uri =
+ "https://example.org/" +
+ ROOT_PATH +
+ "script_redirect.html?" +
+ params.toString();
+ BrowserTestUtils.loadURIString(browser, uri);
+ },
+ permDialogOptions: {
+ checkboxOrigin: ORIGIN1,
+ chooserIsNext: true,
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
+
+/**
+ * Test that we use the redirect principal for the dialog for link clicks.
+ */
+add_task(async function test_redirect_principal_links() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ await testOpenProto(browser, scheme, {
+ triggerLoad: async () => {
+ let uri = `${scheme}://test`;
+
+ let params = new URLSearchParams();
+ params.set("uri", uri);
+ uri =
+ "https://example.com/" +
+ ROOT_PATH +
+ "redirect_helper.sjs?" +
+ params.toString();
+ await ContentTask.spawn(browser, { uri }, args => {
+ let textLink = content.document.createElement("a");
+ textLink.href = args.uri;
+ textLink.textContent = "click me";
+ content.document.body.appendChild(textLink);
+ textLink.click();
+ });
+ },
+ permDialogOptions: {
+ checkboxOrigin: ORIGIN1,
+ chooserIsNext: true,
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});