summaryrefslogtreecommitdiffstats
path: root/uriloader/exthandler/tests/mochitest
diff options
context:
space:
mode:
Diffstat (limited to 'uriloader/exthandler/tests/mochitest')
-rw-r--r--uriloader/exthandler/tests/mochitest/.eslintrc.js5
-rw-r--r--uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js38
-rw-r--r--uriloader/exthandler/tests/mochitest/browser.ini51
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_auto_close_window.js271
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js25
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js612
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js70
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_urlescape.js75
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_extension_correction.js145
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js70
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js255
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js398
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js754
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js76
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_remember_download_option.js61
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js124
-rw-r--r--uriloader/exthandler/tests/mochitest/download.bin1
-rw-r--r--uriloader/exthandler/tests/mochitest/download.sjs38
-rw-r--r--uriloader/exthandler/tests/mochitest/download_page.html22
-rw-r--r--uriloader/exthandler/tests/mochitest/file_as.exe1
-rw-r--r--uriloader/exthandler/tests/mochitest/file_as.exe^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html1
-rw-r--r--uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html1
-rw-r--r--uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf0
-rw-r--r--uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf0
-rw-r--r--uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf0
-rw-r--r--uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt0
-rw-r--r--uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_with@@funny_name.pngbin0 -> 1991 bytes
-rw-r--r--uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_with[funny_name.webmbin0 -> 512 bytes
-rw-r--r--uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml4
-rw-r--r--uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml4
-rw-r--r--uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/handlerApp.xhtml28
-rw-r--r--uriloader/exthandler/tests/mochitest/handlerApps.js118
-rw-r--r--uriloader/exthandler/tests/mochitest/head.js277
-rw-r--r--uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs14
-rw-r--r--uriloader/exthandler/tests/mochitest/mochitest.ini23
-rw-r--r--uriloader/exthandler/tests/mochitest/protocolHandler.html16
-rw-r--r--uriloader/exthandler/tests/mochitest/test_handlerApps.xhtml11
-rw-r--r--uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml47
-rw-r--r--uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml49
-rw-r--r--uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html28
-rw-r--r--uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml72
-rw-r--r--uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs14
51 files changed, 3817 insertions, 0 deletions
diff --git a/uriloader/exthandler/tests/mochitest/.eslintrc.js b/uriloader/exthandler/tests/mochitest/.eslintrc.js
new file mode 100644
index 0000000000..7612459de1
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test", "plugin:mozilla/mochitest-test"],
+};
diff --git a/uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js b/uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js
new file mode 100644
index 0000000000..d08d72b048
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js
@@ -0,0 +1,38 @@
+const { ComponentUtils } = ChromeUtils.import(
+ "resource://gre/modules/ComponentUtils.jsm"
+);
+
+const HELPERAPP_DIALOG_CONTRACT = "@mozilla.org/helperapplauncherdialog;1";
+const HELPERAPP_DIALOG_CID = Components.ID(
+ Cc[HELPERAPP_DIALOG_CONTRACT].number
+);
+
+const FAKE_CID = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator)
+ .generateUUID();
+/* eslint-env mozilla/frame-script */
+function HelperAppLauncherDialog() {}
+HelperAppLauncherDialog.prototype = {
+ show(aLauncher, aWindowContext, aReason) {
+ sendAsyncMessage("suggestedFileName", aLauncher.suggestedFileName);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]),
+};
+
+var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+registrar.registerFactory(
+ FAKE_CID,
+ "",
+ HELPERAPP_DIALOG_CONTRACT,
+ ComponentUtils._getFactory(HelperAppLauncherDialog)
+);
+
+addMessageListener("unregister", function() {
+ registrar.registerFactory(
+ HELPERAPP_DIALOG_CID,
+ "",
+ HELPERAPP_DIALOG_CONTRACT,
+ null
+ );
+ sendAsyncMessage("unregistered");
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser.ini b/uriloader/exthandler/tests/mochitest/browser.ini
new file mode 100644
index 0000000000..0d5b20bef8
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser.ini
@@ -0,0 +1,51 @@
+[DEFAULT]
+head = head.js
+support-files =
+ protocolHandler.html
+
+[browser_auto_close_window.js]
+run-if = e10s # test relies on e10s behavior
+support-files =
+ download_page.html
+ download.bin
+ download.sjs
+[browser_download_always_ask_preferred_app.js]
+[browser_download_privatebrowsing.js]
+[browser_download_open_with_internal_handler.js]
+support-files =
+ file_pdf_application_pdf.pdf
+ file_pdf_application_pdf.pdf^headers^
+ file_pdf_application_unknown.pdf
+ file_pdf_application_unknown.pdf^headers^
+ file_pdf_binary_octet_stream.pdf
+ file_pdf_binary_octet_stream.pdf^headers^
+ file_txt_attachment_test.txt
+ file_txt_attachment_test.txt^headers^
+ file_xml_attachment_binary_octet_stream.xml
+ file_xml_attachment_binary_octet_stream.xml^headers^
+ file_xml_attachment_test.xml
+ file_xml_attachment_test.xml^headers^
+[browser_download_urlescape.js]
+support-files =
+ file_with@@funny_name.png
+ file_with@@funny_name.png^headers^
+ file_with[funny_name.webm
+ file_with[funny_name.webm^headers^
+[browser_extension_correction.js]
+support-files =
+ file_as.exe
+ file_as.exe^headers^
+[browser_open_internal_choice_persistence.js]
+support-files =
+ file_pdf_application_pdf.pdf
+ file_pdf_application_pdf.pdf^headers^
+[browser_protocol_ask_dialog.js]
+support-files =
+ file_nested_protocol_request.html
+[browser_first_prompt_not_blocked_without_user_interaction.js]
+support-files =
+ file_external_protocol_iframe.html
+[browser_protocol_ask_dialog_permission.js]
+[browser_protocolhandler_loop.js]
+[browser_remember_download_option.js]
+[browser_web_protocol_handlers.js]
diff --git a/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js b/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js
new file mode 100644
index 0000000000..2e1d17e139
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js
@@ -0,0 +1,271 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { ComponentUtils } = ChromeUtils.import(
+ "resource://gre/modules/ComponentUtils.jsm"
+);
+
+const ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const PAGE_URL = ROOT + "download_page.html";
+const SJS_URL = ROOT + "download.sjs";
+
+const HELPERAPP_DIALOG_CONTRACT_ID = "@mozilla.org/helperapplauncherdialog;1";
+const HELPERAPP_DIALOG_CID = Components.ID(
+ Cc[HELPERAPP_DIALOG_CONTRACT_ID].number
+);
+const MOCK_HELPERAPP_DIALOG_CID = Components.ID(
+ "{2f372b6f-56c9-46d5-af0d-9f09bb69860c}"
+);
+
+let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+let curDialogResolve = null;
+
+function HelperAppLauncherDialog() {}
+
+HelperAppLauncherDialog.prototype = {
+ show(aLauncher, aWindowContext, aReason) {
+ ok(true, "Showing the helper app dialog");
+ curDialogResolve(aWindowContext);
+ executeSoon(() => {
+ aLauncher.cancel(Cr.NS_ERROR_ABORT);
+ });
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]),
+};
+
+function promiseHelperAppDialog() {
+ return new Promise(resolve => {
+ curDialogResolve = resolve;
+ });
+}
+
+let mockHelperAppService;
+
+add_task(async function setup() {
+ // Replace the real helper app dialog with our own.
+ mockHelperAppService = ComponentUtils._getFactory(HelperAppLauncherDialog);
+ registrar.registerFactory(
+ MOCK_HELPERAPP_DIALOG_CID,
+ "",
+ HELPERAPP_DIALOG_CONTRACT_ID,
+ mockHelperAppService
+ );
+});
+
+add_task(async function simple_navigation() {
+ // Tests that simple navigation gives us the right windowContext (that is,
+ // the window that we're using).
+ await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function(
+ browser
+ ) {
+ let dialogAppeared = promiseHelperAppDialog();
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#regular_load",
+ {},
+ browser
+ );
+ let windowContext = await dialogAppeared;
+
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+ });
+});
+
+// Given a browser pointing to download_page.html, clicks on the link that
+// opens with target="_blank" (i.e. a new tab) and ensures that we
+// automatically open and close that tab.
+async function testNewTab(browser) {
+ let dialogAppeared = promiseHelperAppDialog();
+ let tabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ ).then(event => {
+ return [event.target, BrowserTestUtils.waitForTabClosing(event.target)];
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter("#target_blank", {}, browser);
+
+ let windowContext = await dialogAppeared;
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+ let [tab, closingPromise] = await tabOpened;
+ await closingPromise;
+ is(tab.linkedBrowser, null, "tab was opened and closed");
+}
+
+add_task(async function target_blank() {
+ // Tests that a link with target=_blank opens a new tab and closes it,
+ // returning the window that we're using for navigation.
+ await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function(
+ browser
+ ) {
+ await testNewTab(browser);
+ });
+});
+
+add_task(async function target_blank_no_opener() {
+ // Tests that a link with target=_blank and no opener opens a new tab
+ // and closes it, returning the window that we're using for navigation.
+ await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function(
+ browser
+ ) {
+ let dialogAppeared = promiseHelperAppDialog();
+ let tabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ ).then(event => {
+ return [event.target, BrowserTestUtils.waitForTabClosing(event.target)];
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#target_blank_no_opener",
+ {},
+ browser
+ );
+
+ let windowContext = await dialogAppeared;
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+ let [tab, closingPromise] = await tabOpened;
+ await closingPromise;
+ is(tab.linkedBrowser, null, "tab was opened and closed");
+ });
+});
+
+add_task(async function open_in_new_tab_no_opener() {
+ // Tests that a link with target=_blank and no opener opens a new tab
+ // and closes it, returning the window that we're using for navigation.
+ await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function(
+ browser
+ ) {
+ let dialogAppeared = promiseHelperAppDialog();
+ let tabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ ).then(event => {
+ return [event.target, BrowserTestUtils.waitForTabClosing(event.target)];
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#open_in_new_tab_no_opener",
+ {},
+ browser
+ );
+
+ let windowContext = await dialogAppeared;
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+ let [tab, closingPromise] = await tabOpened;
+ await closingPromise;
+ is(tab.linkedBrowser, null, "tab was opened and closed");
+ });
+});
+
+add_task(async function new_window() {
+ // Tests that a link that forces us to open a new window (by specifying a
+ // width and a height in window.open) opens a new window for the load,
+ // realizes that we need to close that window and returns the *original*
+ // window as the window context.
+ await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function(
+ browser
+ ) {
+ let dialogAppeared = promiseHelperAppDialog();
+ let windowOpened = BrowserTestUtils.waitForNewWindow();
+
+ await BrowserTestUtils.synthesizeMouseAtCenter("#new_window", {}, browser);
+ let win = await windowOpened;
+ // Now allow request to complete:
+ fetch(SJS_URL + "?finish");
+
+ let windowContext = await dialogAppeared;
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+
+ // The window should close on its own. If not, this test will time out.
+ await BrowserTestUtils.domWindowClosed(win);
+ ok(win.closed, "window was opened and closed");
+
+ is(
+ await fetch(SJS_URL + "?reset").then(r => r.text()),
+ "OK",
+ "Test reseted"
+ );
+ });
+});
+
+add_task(async function new_window_no_opener() {
+ // Tests that a link that forces us to open a new window (by specifying a
+ // width and a height in window.open) opens a new window for the load,
+ // realizes that we need to close that window and returns the *original*
+ // window as the window context.
+ await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function(
+ browser
+ ) {
+ let dialogAppeared = promiseHelperAppDialog();
+ let windowOpened = BrowserTestUtils.waitForNewWindow();
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#new_window_no_opener",
+ {},
+ browser
+ );
+ let win = await windowOpened;
+ // Now allow request to complete:
+ fetch(SJS_URL + "?finish");
+
+ await dialogAppeared;
+
+ // The window should close on its own. If not, this test will time out.
+ await BrowserTestUtils.domWindowClosed(win);
+ ok(win.closed, "window was opened and closed");
+
+ is(
+ await fetch(SJS_URL + "?reset").then(r => r.text()),
+ "OK",
+ "Test reseted"
+ );
+ });
+});
+
+add_task(async function nested_window_opens() {
+ // Tests that the window auto-closing feature works if the download is
+ // initiated by a window that, itself, has an opener (see bug 1373109).
+ await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function(
+ outerBrowser
+ ) {
+ let secondTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ `${PAGE_URL}?newwin`,
+ true
+ );
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#open_in_new_tab",
+ {},
+ outerBrowser
+ );
+ let secondTab = await secondTabPromise;
+ let nestedBrowser = secondTab.linkedBrowser;
+
+ await SpecialPowers.spawn(nestedBrowser, [], function() {
+ ok(content.opener, "this window has an opener");
+ });
+
+ await testNewTab(nestedBrowser);
+
+ isnot(
+ secondTab.linkedBrowser,
+ null,
+ "the page that triggered the download is still open"
+ );
+ BrowserTestUtils.removeTab(secondTab);
+ });
+});
+
+add_task(async function cleanup() {
+ // Unregister our factory from XPCOM and restore the original CID.
+ registrar.unregisterFactory(MOCK_HELPERAPP_DIALOG_CID, mockHelperAppService);
+ registrar.registerFactory(
+ HELPERAPP_DIALOG_CID,
+ "",
+ HELPERAPP_DIALOG_CONTRACT_ID,
+ null
+ );
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js b/uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js
new file mode 100644
index 0000000000..bd421d51f3
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js
@@ -0,0 +1,25 @@
+add_task(async function() {
+ // Create mocked objects for test
+ let launcher = createMockedObjects(false);
+ // Open helper app dialog with mocked launcher
+ let dlg = await openHelperAppDialog(launcher);
+ let doc = dlg.document;
+ let location = doc.getElementById("source");
+ let expectedValue = launcher.source.prePath;
+ if (location.value != expectedValue) {
+ info("Waiting for dialog to be populated.");
+ await BrowserTestUtils.waitForAttribute("value", location, expectedValue);
+ }
+ is(
+ doc.getElementById("mode").selectedItem.id,
+ "open",
+ "Should be opening the file."
+ );
+ ok(
+ !dlg.document.getElementById("openHandler").selectedItem.hidden,
+ "Should not have selected a hidden item."
+ );
+ let helperAppDialogHiddenPromise = BrowserTestUtils.windowClosed(dlg);
+ doc.getElementById("unknownContentType").cancelDialog();
+ await helperAppDialogHiddenPromise;
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js b/uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js
new file mode 100644
index 0000000000..ef7174f30f
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js
@@ -0,0 +1,612 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/Downloads.jsm", this);
+const { DownloadIntegration } = ChromeUtils.import(
+ "resource://gre/modules/DownloadIntegration.jsm"
+);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+function waitForAcceptButtonToGetEnabled(doc) {
+ let dialog = doc.querySelector("#unknownContentType");
+ let button = dialog.getButton("accept");
+ return TestUtils.waitForCondition(
+ () => !button.disabled,
+ "Wait for Accept button to get enabled"
+ );
+}
+
+async function waitForPdfJS(browser, url) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["pdfjs.eventBusDispatchToDOM", true]],
+ });
+ // Runs tests after all "load" event handlers have fired off
+ let loadPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "documentloaded",
+ false,
+ null,
+ true
+ );
+ await SpecialPowers.spawn(browser, [url], contentUrl => {
+ content.location = contentUrl;
+ });
+ return loadPromise;
+}
+
+add_task(async function setup() {
+ // Remove the security delay for the dialog during the test.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.dialog_enable_delay", 0],
+ ["browser.helperApps.showOpenOptionForPdfJS", true],
+ ["browser.helperApps.showOpenOptionForViewableInternally", true],
+ ],
+ });
+
+ // Restore handlers after the whole test has run
+ const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+ const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ const registerRestoreHandler = function(type, ext) {
+ const mimeInfo = mimeSvc.getFromTypeAndExtension(type, ext);
+ const existed = handlerSvc.exists(mimeInfo);
+ registerCleanupFunction(() => {
+ if (existed) {
+ handlerSvc.store(mimeInfo);
+ } else {
+ handlerSvc.remove(mimeInfo);
+ }
+ });
+ };
+ registerRestoreHandler("application/pdf", "pdf");
+ registerRestoreHandler("binary/octet-stream", "pdf");
+ registerRestoreHandler("application/unknown", "pdf");
+});
+
+/**
+ * Check that loading a PDF file with content-disposition: attachment
+ * shows an option to open with the internal handler, and that the
+ * internal option handler is not present when the download button
+ * is clicked from pdf.js.
+ */
+add_task(async function test_check_open_with_internal_handler() {
+ for (let file of [
+ "file_pdf_application_pdf.pdf",
+ "file_pdf_binary_octet_stream.pdf",
+ ]) {
+ info("Testing with " + file);
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + file
+ );
+ // Add an extra tab after the loading tab so we can test that
+ // pdf.js is opened in the adjacent tab and not at the end of
+ // the tab strip.
+ let extraTab = await BrowserTestUtils.addTab(gBrowser, "about:blank");
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+ let doc = dialogWindow.document;
+ let internalHandlerRadio = doc.querySelector("#handleInternally");
+
+ await waitForAcceptButtonToGetEnabled(doc);
+
+ ok(!internalHandlerRadio.hidden, "The option should be visible for PDF");
+ ok(internalHandlerRadio.selected, "The option should be selected");
+
+ let downloadFinishedPromise = promiseDownloadFinished(publicList);
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+ let dialog = doc.querySelector("#unknownContentType");
+ let button = dialog.getButton("accept");
+ button.disabled = false;
+ dialog.acceptDialog();
+ info("waiting for new tab to open");
+ let newTab = await newTabPromise;
+
+ is(
+ newTab._tPos - 1,
+ loadingTab._tPos,
+ "pdf.js should be opened in an adjacent tab"
+ );
+
+ await ContentTask.spawn(newTab.linkedBrowser, null, async () => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.readyState == "complete"
+ );
+ });
+
+ let publicDownloads = await publicList.getAll();
+ is(
+ publicDownloads.length,
+ 1,
+ "download should appear in publicDownloads list"
+ );
+
+ let download = await downloadFinishedPromise;
+
+ let subdialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ // Current tab has file: URI and TEST_PATH is http uri, so uri will be different
+ BrowserTestUtils.loadURI(newTab.linkedBrowser, TEST_PATH + file);
+ let subDialogWindow = await subdialogPromise;
+ let subDoc = subDialogWindow.document;
+ // Prevent racing with initialization of the dialog and make sure that
+ // the final state of the dialog has the correct visibility of the internal-handler option.
+ await waitForAcceptButtonToGetEnabled(subDoc);
+ let subInternalHandlerRadio = subDoc.querySelector("#handleInternally");
+ ok(
+ !subInternalHandlerRadio.hidden,
+ "This option should be shown when the dialog is shown for another PDF"
+ );
+ // Cancel dialog
+ subDoc.querySelector("#unknownContentType").cancelDialog();
+
+ subdialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ await SpecialPowers.spawn(newTab.linkedBrowser, [], async () => {
+ let downloadButton;
+ await ContentTaskUtils.waitForCondition(() => {
+ downloadButton = content.document.querySelector("#download");
+ return !!downloadButton;
+ });
+ ok(downloadButton, "Download button should be present in pdf.js");
+ downloadButton.click();
+ });
+ info(
+ "Waiting for unknown content type dialog to appear from pdf.js download button click"
+ );
+ subDialogWindow = await subdialogPromise;
+ subDoc = subDialogWindow.document;
+ // Prevent racing with initialization of the dialog and make sure that
+ // the final state of the dialog has the correct visibility of the internal-handler option.
+ await waitForAcceptButtonToGetEnabled(subDoc);
+ subInternalHandlerRadio = subDoc.querySelector("#handleInternally");
+ ok(
+ subInternalHandlerRadio.hidden,
+ "The option should be hidden when the dialog is opened from pdf.js"
+ );
+ subDoc.querySelector("#open").click();
+
+ let tabOpenListener = () => {
+ ok(
+ false,
+ "A new tab should not be opened when accepting the dialog with 'open-with-external-app' chosen"
+ );
+ };
+ gBrowser.tabContainer.addEventListener("TabOpen", tabOpenListener);
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ let waitForLaunchFileCalled = new Promise(resolve => {
+ DownloadIntegration.launchFile = async () => {
+ ok(true, "The file should be launched with an external application");
+ resolve();
+ };
+ });
+
+ downloadFinishedPromise = promiseDownloadFinished(publicList);
+
+ info("Accepting the dialog");
+ subDoc.querySelector("#unknownContentType").acceptDialog();
+ info("Waiting until DownloadIntegration.launchFile is called");
+ await waitForLaunchFileCalled;
+ DownloadIntegration.launchFile = oldLaunchFile;
+
+ // Remove the first file (can't do this sooner or the second load fails):
+ if (download?.target.exists) {
+ try {
+ info("removing " + download.target.path);
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {
+ /* ignore */
+ }
+ }
+
+ gBrowser.tabContainer.removeEventListener("TabOpen", tabOpenListener);
+ BrowserTestUtils.removeTab(loadingTab);
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(extraTab);
+
+ // Remove the remaining file once complete.
+ download = await downloadFinishedPromise;
+ if (download?.target.exists) {
+ try {
+ info("removing " + download.target.path);
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {
+ /* ignore */
+ }
+ }
+ await publicList.removeFinished();
+ }
+});
+
+/**
+ * Test that choosing to open in an external application doesn't
+ * open the PDF into pdf.js
+ */
+add_task(async function test_check_open_with_external_application() {
+ for (let file of [
+ "file_pdf_application_pdf.pdf",
+ "file_pdf_binary_octet_stream.pdf",
+ ]) {
+ info("Testing with " + file);
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + file
+ );
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ let waitForLaunchFileCalled = new Promise(resolve => {
+ DownloadIntegration.launchFile = () => {
+ ok(true, "The file should be launched with an external application");
+ resolve();
+ };
+ });
+
+ let doc = dialogWindow.document;
+ await waitForAcceptButtonToGetEnabled(doc);
+ let dialog = doc.querySelector("#unknownContentType");
+ doc.querySelector("#open").click();
+ let button = dialog.getButton("accept");
+ button.disabled = false;
+ info("Accepting the dialog");
+ dialog.acceptDialog();
+ info("Waiting until DownloadIntegration.launchFile is called");
+ await waitForLaunchFileCalled;
+ DownloadIntegration.launchFile = oldLaunchFile;
+
+ let publicDownloads = await publicList.getAll();
+ is(
+ publicDownloads.length,
+ 1,
+ "download should appear in publicDownloads list"
+ );
+ let download = publicDownloads[0];
+ ok(
+ !download.launchWhenSucceeded,
+ "launchWhenSucceeded should be false after launchFile is called"
+ );
+
+ BrowserTestUtils.removeTab(loadingTab);
+ if (download?.target.exists) {
+ try {
+ info("removing " + download.target.path);
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {
+ /* ignore */
+ }
+ }
+ await publicList.removeFinished();
+ }
+});
+
+/**
+ * Test that choosing to open a PDF with an external application works and
+ * then downloading the same file again and choosing Open with Firefox opens
+ * the download in Firefox.
+ */
+add_task(async function test_check_open_with_external_then_internal() {
+ // This test only runs on Windows because appPicker.xhtml is only used on Windows.
+ if (AppConstants.platform != "win") {
+ return;
+ }
+
+ // This test covers a bug that only occurs when the mimeInfo is set to Always Ask
+ const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+ const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ const mimeInfo = mimeSvc.getFromTypeAndExtension("application/pdf", "pdf");
+ mimeInfo.preferredAction = mimeInfo.alwaysAsk;
+ mimeInfo.alwaysAskBeforeHandling = true;
+ handlerSvc.store(mimeInfo);
+
+ for (let [file, mimeType] of [
+ ["file_pdf_application_pdf.pdf", "application/pdf"],
+ ["file_pdf_binary_octet_stream.pdf", "binary/octet-stream"],
+ ["file_pdf_application_unknown.pdf", "application/unknown"],
+ ]) {
+ info("Testing with " + file);
+ let originalMimeInfo = mimeSvc.getFromTypeAndExtension(mimeType, "pdf");
+
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ // Open a new tab to the PDF file which will trigger the Unknown Content Type dialog
+ // and choose to open the PDF with an external application.
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + file
+ );
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ let waitForLaunchFileCalled = new Promise(resolve => {
+ DownloadIntegration.launchFile = () => {
+ ok(true, "The file should be launched with an external application");
+ resolve();
+ };
+ });
+
+ let doc = dialogWindow.document;
+ await waitForAcceptButtonToGetEnabled(doc);
+ let dialog = doc.querySelector("#unknownContentType");
+ let openHandlerMenulist = doc.querySelector("#openHandler");
+ let originalDefaultHandler = openHandlerMenulist.label;
+ doc.querySelector("#open").click();
+ doc.querySelector("#openHandlerPopup").click();
+ let oldOpenDialog = dialogWindow.openDialog;
+ dialogWindow.openDialog = (location, unused2, unused3, params) => {
+ is(location, "chrome://global/content/appPicker.xhtml", "app picker");
+ let handlerApp = params.mimeInfo.possibleLocalHandlers.queryElementAt(
+ 0,
+ Ci.nsILocalHandlerApp
+ );
+ ok(handlerApp.executable, "handlerApp should be executable");
+ ok(handlerApp.executable.isFile(), "handlerApp should be a file");
+ params.handlerApp = handlerApp;
+ };
+ doc.querySelector("#choose").click();
+ dialogWindow.openDialog = oldOpenDialog;
+ await TestUtils.waitForCondition(
+ () => originalDefaultHandler != openHandlerMenulist.label,
+ "waiting for openHandler to get updated"
+ );
+ let newDefaultHandler = openHandlerMenulist.label;
+ info(`was ${originalDefaultHandler}, now ${newDefaultHandler}`);
+ let button = dialog.getButton("accept");
+ button.disabled = false;
+ info("Accepting the dialog");
+ dialog.acceptDialog();
+ info("Waiting until DownloadIntegration.launchFile is called");
+ await waitForLaunchFileCalled;
+ BrowserTestUtils.removeTab(loadingTab);
+
+ // Now, open a new tab to the PDF file which will trigger the Unknown Content Type dialog
+ // and choose to open the PDF internally. The previously used external application should be shown as
+ // the external option.
+ dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ loadingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + file
+ );
+ dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+
+ DownloadIntegration.launchFile = () => {
+ ok(false, "The file should not be launched with an external application");
+ };
+
+ doc = dialogWindow.document;
+ await waitForAcceptButtonToGetEnabled(doc);
+ openHandlerMenulist = doc.querySelector("#openHandler");
+ is(openHandlerMenulist.label, newDefaultHandler, "'new' handler");
+ dialog = doc.querySelector("#unknownContentType");
+ doc.querySelector("#handleInternally").click();
+ info("Accepting the dialog");
+ button = dialog.getButton("accept");
+ button.disabled = false;
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+ dialog.acceptDialog();
+
+ info("waiting for new tab to open");
+ let newTab = await newTabPromise;
+
+ await ContentTask.spawn(newTab.linkedBrowser, null, async () => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.readyState == "complete"
+ );
+ });
+
+ is(
+ newTab.linkedBrowser.contentPrincipal.origin,
+ "resource://pdf.js",
+ "PDF should be opened with pdf.js"
+ );
+
+ BrowserTestUtils.removeTab(loadingTab);
+ BrowserTestUtils.removeTab(newTab);
+
+ // Now trigger the dialog again and select the system
+ // default option to reset the state for the next iteration of the test.
+ // Reset the state for the next iteration of the test.
+ handlerSvc.store(originalMimeInfo);
+ DownloadIntegration.launchFile = oldLaunchFile;
+ let [download] = await publicList.getAll();
+ if (download?.target.exists) {
+ try {
+ info("removing " + download.target.path);
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {
+ /* ignore */
+ }
+ }
+ await publicList.removeFinished();
+ }
+});
+
+/**
+ * Check that the "Open with internal handler" option is presented
+ * for other viewable internally types.
+ */
+add_task(
+ async function test_internal_handler_hidden_with_viewable_internally_type() {
+ for (let [file, checkDefault] of [
+ // The default for binary/octet-stream is changed by the PDF tests above,
+ // this may change given bug 1659008, so I'm just ignoring the default for now.
+ ["file_xml_attachment_binary_octet_stream.xml", false],
+ ["file_xml_attachment_test.xml", true],
+ ]) {
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + file
+ );
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+ let doc = dialogWindow.document;
+ let internalHandlerRadio = doc.querySelector("#handleInternally");
+
+ // Prevent racing with initialization of the dialog and make sure that
+ // the final state of the dialog has the correct visibility of the internal-handler option.
+ await waitForAcceptButtonToGetEnabled(doc);
+
+ ok(!internalHandlerRadio.hidden, "The option should be visible for XML");
+ if (checkDefault) {
+ ok(internalHandlerRadio.selected, "The option should be selected");
+ }
+
+ let dialog = doc.querySelector("#unknownContentType");
+ dialog.cancelDialog();
+ BrowserTestUtils.removeTab(loadingTab);
+ }
+ }
+);
+
+/**
+ * Check that the "Open with internal handler" option is not presented
+ * for non-PDF, non-viewable-internally types.
+ */
+add_task(async function test_internal_handler_hidden_with_other_type() {
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + "file_txt_attachment_test.txt"
+ );
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+ let doc = dialogWindow.document;
+
+ // Prevent racing with initialization of the dialog and make sure that
+ // the final state of the dialog has the correct visibility of the internal-handler option.
+ await waitForAcceptButtonToGetEnabled(doc);
+
+ let internalHandlerRadio = doc.querySelector("#handleInternally");
+ ok(
+ internalHandlerRadio.hidden,
+ "The option should be hidden for unknown file type"
+ );
+
+ let dialog = doc.querySelector("#unknownContentType");
+ dialog.cancelDialog();
+ BrowserTestUtils.removeTab(loadingTab);
+});
+
+/**
+ * Check that the "Open with internal handler" option is not presented
+ * when the feature is disabled for PDFs.
+ */
+add_task(async function test_internal_handler_hidden_with_pdf_pref_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.helperApps.showOpenOptionForPdfJS", false]],
+ });
+ for (let file of [
+ "file_pdf_application_pdf.pdf",
+ "file_pdf_binary_octet_stream.pdf",
+ ]) {
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + file
+ );
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+ let doc = dialogWindow.document;
+
+ await waitForAcceptButtonToGetEnabled(doc);
+
+ let internalHandlerRadio = doc.querySelector("#handleInternally");
+ ok(
+ internalHandlerRadio.hidden,
+ "The option should be hidden for PDF when the pref is false"
+ );
+
+ let dialog = doc.querySelector("#unknownContentType");
+ dialog.cancelDialog();
+ BrowserTestUtils.removeTab(loadingTab);
+ }
+});
+
+/**
+ * Check that the "Open with internal handler" option is not presented
+ * for other viewable internally types when disabled.
+ */
+add_task(
+ async function test_internal_handler_hidden_with_viewable_internally_pref_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.helperApps.showOpenOptionForViewableInternally", false]],
+ });
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + "file_xml_attachment_test.xml"
+ );
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+ let doc = dialogWindow.document;
+
+ await waitForAcceptButtonToGetEnabled(doc);
+
+ let internalHandlerRadio = doc.querySelector("#handleInternally");
+ ok(
+ internalHandlerRadio.hidden,
+ "The option should be hidden for XML when the pref is false"
+ );
+
+ let dialog = doc.querySelector("#unknownContentType");
+ dialog.cancelDialog();
+ BrowserTestUtils.removeTab(loadingTab);
+ }
+);
diff --git a/uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js b/uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js
new file mode 100644
index 0000000000..02cf6c3941
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that downloads started from a private window by clicking on a link end
+ * up in the global list of private downloads (see bug 1367581).
+ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/Downloads.jsm", this);
+ChromeUtils.import("resource://gre/modules/DownloadPaths.jsm", this);
+ChromeUtils.import("resource://testing-common/FileTestUtils.jsm", this);
+ChromeUtils.import("resource://testing-common/MockRegistrar.jsm", this);
+
+add_task(async function test_setup() {
+ // Save downloads to disk without showing the dialog.
+ let cid = MockRegistrar.register("@mozilla.org/helperapplauncherdialog;1", {
+ QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]),
+ show(launcher) {
+ launcher.promptForSaveDestination();
+ },
+ promptForSaveToFileAsync(launcher) {
+ // The dialog should create the empty placeholder file.
+ let file = FileTestUtils.getTempFile();
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ launcher.saveDestinationAvailable(file);
+ },
+ });
+ registerCleanupFunction(() => {
+ MockRegistrar.unregister(cid);
+ });
+});
+
+add_task(async function test_download_privatebrowsing() {
+ let privateList = await Downloads.getList(Downloads.PRIVATE);
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ try {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ `data:text/html,<a download href="data:text/plain,">download</a>`
+ );
+
+ let promiseNextPrivateDownload = new Promise(resolve => {
+ privateList.addView({
+ onDownloadAdded(download) {
+ privateList.removeView(this);
+ resolve(download);
+ },
+ });
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
+ content.document.querySelector("a").click();
+ });
+
+ // Wait for the download to finish so the file can be safely deleted later.
+ let download = await promiseNextPrivateDownload;
+ await download.whenSucceeded();
+
+ // Clean up after checking that there are no new public downloads either.
+ let publicDownloads = await publicList.getAll();
+ Assert.equal(publicDownloads.length, 0);
+ await privateList.removeFinished();
+ } finally {
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_download_urlescape.js b/uriloader/exthandler/tests/mochitest/browser_download_urlescape.js
new file mode 100644
index 0000000000..ffab8146b6
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_download_urlescape.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+registerCleanupFunction(() => MockFilePicker.cleanup());
+
+/**
+ * Check downloading files URL-escapes content-disposition
+ * information when necessary.
+ */
+add_task(async function test_check_filename_urlescape() {
+ let pendingPromise;
+ let pendingTest = "";
+ let expectedFileName = "";
+ MockFilePicker.showCallback = function(fp) {
+ info(`${pendingTest} - Filepicker shown, checking filename`);
+ is(
+ fp.defaultString,
+ expectedFileName,
+ `${pendingTest} - Should have escaped filename`
+ );
+ ok(
+ pendingPromise,
+ `${pendingTest} - Should have expected this picker open.`
+ );
+ if (pendingPromise) {
+ pendingPromise.resolve();
+ }
+ return Ci.nsIFilePicker.returnCancel;
+ };
+ function runTestFor(fileName, selector) {
+ return BrowserTestUtils.withNewTab(TEST_PATH + fileName, async browser => {
+ expectedFileName = fileName;
+ let tabLabel = gBrowser.getTabForBrowser(browser).getAttribute("label");
+ ok(
+ tabLabel.startsWith(fileName),
+ `"${tabLabel}" should have been escaped.`
+ );
+
+ pendingTest = "save browser";
+ pendingPromise = PromiseUtils.defer();
+ // First try to save the browser
+ saveBrowser(browser);
+ await pendingPromise.promise;
+
+ // Next, try the context menu:
+ pendingTest = "save from context menu";
+ pendingPromise = PromiseUtils.defer();
+ let menu = document.getElementById("contentAreaContextMenu");
+ let menuShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ BrowserTestUtils.synthesizeMouse(
+ selector,
+ 5,
+ 5,
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await menuShown;
+ gContextMenu.saveMedia();
+ menu.hidePopup();
+ await pendingPromise.promise;
+ pendingPromise = null;
+ });
+ }
+ await runTestFor("file_with@@funny_name.png", "img");
+ await runTestFor("file_with[funny_name.webm", "video");
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_extension_correction.js b/uriloader/exthandler/tests/mochitest/browser_extension_correction.js
new file mode 100644
index 0000000000..b806ee9ace
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_extension_correction.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+let gPathsToRemove = [];
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.useDownloadDir", true]],
+ });
+ registerCleanupFunction(async () => {
+ for (let path of gPathsToRemove) {
+ // IOUtils.remove ignores non-existing files out of the box.
+ await IOUtils.remove(path);
+ }
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ await publicList.removeFinished();
+ });
+});
+
+async function testLinkWithoutExtension(type, shouldHaveExtension) {
+ info("Checking " + type);
+
+ let task = function() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [type], mimetype => {
+ let link = content.document.createElement("a");
+ link.textContent = "Click me";
+ link.href = "data:" + mimetype + ",hello";
+ link.download = "somefile";
+ content.document.body.appendChild(link);
+ link.click();
+ });
+ };
+ await checkDownloadWithExtensionState(task, { type, shouldHaveExtension });
+}
+
+async function checkDownloadWithExtensionState(
+ task,
+ { type, shouldHaveExtension, expectedName = null }
+) {
+ let winPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+
+ await task();
+
+ info("Waiting for dialog.");
+ let win = await winPromise;
+
+ let actualName = win.document.getElementById("location").value;
+ if (shouldHaveExtension) {
+ expectedName ??= "somefile." + getMIMEInfoForType(type).primaryExtension;
+ is(actualName, expectedName, `${type} should get an extension`);
+ } else {
+ expectedName ??= "somefile";
+ is(actualName, expectedName, `${type} should not get an extension`);
+ }
+
+ let closedPromise = BrowserTestUtils.windowClosed(win);
+
+ if (shouldHaveExtension) {
+ // Wait for the download.
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloadFinishedPromise = promiseDownloadFinished(publicList);
+
+ // Then pick "save" in the dialog.
+ let dialog = win.document.getElementById("unknownContentType");
+ win.document.getElementById("save").click();
+ let button = dialog.getButton("accept");
+ button.disabled = false;
+ dialog.acceptDialog();
+
+ // Wait for the download to finish and check the extension is correct.
+ let download = await downloadFinishedPromise;
+ is(
+ PathUtils.filename(download.target.path),
+ expectedName,
+ `Downloaded file should also match ${expectedName}`
+ );
+ gPathsToRemove.push(download.target.path);
+ let pathToRemove = download.target.path;
+ // Avoid one file interfering with subsequent files.
+ await publicList.removeFinished();
+ await IOUtils.remove(pathToRemove);
+ } else {
+ // We just cancel out for files that would end up without a path, as we'd
+ // prompt for a filename.
+ win.close();
+ }
+ return closedPromise;
+}
+
+/**
+ * Check that for document types, images, videos and audio files,
+ * we enforce a useful extension.
+ */
+add_task(async function test_enforce_useful_extension() {
+ await BrowserTestUtils.withNewTab("data:text/html,", async browser => {
+ await testLinkWithoutExtension("image/png", true);
+ await testLinkWithoutExtension("audio/ogg", true);
+ await testLinkWithoutExtension("video/webm", true);
+ await testLinkWithoutExtension("application/msword", true);
+ await testLinkWithoutExtension("application/pdf", true);
+
+ await testLinkWithoutExtension("application/x-gobbledygook", false);
+ await testLinkWithoutExtension("application/octet-stream", false);
+ await testLinkWithoutExtension("binary/octet-stream", false);
+ await testLinkWithoutExtension("application/x-msdownload", false);
+ });
+});
+
+/**
+ * Check that we still use URL extension info when we don't have anything else,
+ * despite bogus local info.
+ */
+add_task(async function test_broken_saved_handlerinfo_and_useless_mimetypes() {
+ let bogusType = getMIMEInfoForType("binary/octet-stream");
+ bogusType.setFileExtensions(["jpg"]);
+ let handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ handlerSvc.store(bogusType);
+ let tabToClean = null;
+ let task = function() {
+ return BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + "file_as.exe?foo=bar"
+ ).then(tab => {
+ return (tabToClean = tab);
+ });
+ };
+ await checkDownloadWithExtensionState(task, {
+ type: "binary/octet-stream",
+ shouldHaveExtension: true,
+ expectedName: "file_as.exe",
+ });
+ // Downloads should really close their tabs...
+ if (tabToClean?.isConnected) {
+ BrowserTestUtils.removeTab(tabToClean);
+ }
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js b/uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js
new file mode 100644
index 0000000000..b6f401e5e1
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+add_task(setupMailHandler);
+
+add_task(async function test_open_without_user_interaction() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.disable_open_during_load", true],
+ ["dom.block_external_protocol_in_iframes", true],
+ ["dom.delay.block_external_protocol_in_iframes.enabled", false],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+
+ BrowserTestUtils.loadURI(
+ tab.linkedBrowser,
+ TEST_PATH + "file_external_protocol_iframe.html"
+ );
+
+ let dialog = await dialogWindowPromise;
+ ok(dialog, "Should show the dialog even without user interaction");
+
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+
+ // Adding another iframe without user interaction should be blocked.
+ let blockedWarning = new Promise(resolve => {
+ Services.console.registerListener(function onMessage(msg) {
+ let { message, logLevel } = msg;
+ if (logLevel != Ci.nsIConsoleMessage.warn) {
+ return;
+ }
+ if (!message.includes("Iframe with external protocol was blocked")) {
+ return;
+ }
+ Services.console.unregisterListener(onMessage);
+ resolve();
+ });
+ });
+
+ info("Adding another frame without user interaction");
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
+ let frame = content.document.createElement("iframe");
+ frame.src = "mailto:foo@baz.com";
+ content.document.body.appendChild(frame);
+ });
+
+ await blockedWarning;
+
+ info("Removing tab to close the dialog.");
+ gBrowser.removeTab(tab);
+ await dialogClosedPromise;
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js b/uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js
new file mode 100644
index 0000000000..a36443edd1
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js
@@ -0,0 +1,255 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/Downloads.jsm", this);
+const { DownloadIntegration } = ChromeUtils.import(
+ "resource://gre/modules/DownloadIntegration.jsm"
+);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+function waitForAcceptButtonToGetEnabled(doc) {
+ let dialog = doc.querySelector("#unknownContentType");
+ let button = dialog.getButton("accept");
+ return TestUtils.waitForCondition(
+ () => !button.disabled,
+ "Wait for Accept button to get enabled"
+ );
+}
+
+async function waitForPdfJS(browser, url) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["pdfjs.eventBusDispatchToDOM", true]],
+ });
+ // Runs tests after all "load" event handlers have fired off
+ let loadPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "documentloaded",
+ false,
+ null,
+ true
+ );
+ await SpecialPowers.spawn(browser, [url], contentUrl => {
+ content.location = contentUrl;
+ });
+ return loadPromise;
+}
+
+add_task(async function setup() {
+ // Remove the security delay for the dialog during the test.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.dialog_enable_delay", 0],
+ ["browser.helperApps.showOpenOptionForPdfJS", true],
+ ["browser.helperApps.showOpenOptionForViewableInternally", true],
+ ],
+ });
+
+ // Restore handlers after the whole test has run
+ const registerRestoreHandler = function(type, ext) {
+ const mimeInfo = gMimeSvc.getFromTypeAndExtension(type, ext);
+ const existed = gHandlerSvc.exists(mimeInfo);
+ registerCleanupFunction(() => {
+ if (existed) {
+ gHandlerSvc.store(mimeInfo);
+ } else {
+ gHandlerSvc.remove(mimeInfo);
+ }
+ });
+ };
+ registerRestoreHandler("application/pdf", "pdf");
+});
+
+const { handleInternally, saveToDisk, useSystemDefault } = Ci.nsIHandlerInfo;
+
+const kTestCases = [
+ {
+ description:
+ "Saving to disk when internal handling is the default shouldn't change prefs.",
+ preDialogState: { preferredAction: handleInternally, alwaysAsk: false },
+ dialogActions(doc) {
+ let saveItem = doc.querySelector("#save");
+ saveItem.click();
+ ok(saveItem.selected, "The 'save' option should now be selected");
+ },
+ expectTab: false,
+ expectLaunch: false,
+ expectedPreferredAction: handleInternally,
+ expectedAsk: false,
+ },
+ {
+ description:
+ "Opening externally when internal handling is the default shouldn't change prefs.",
+ preDialogState: { preferredAction: handleInternally, alwaysAsk: false },
+ dialogActions(doc) {
+ let openItem = doc.querySelector("#open");
+ openItem.click();
+ ok(openItem.selected, "The 'save' option should now be selected");
+ },
+ expectTab: false,
+ expectLaunch: true,
+ expectedPreferredAction: handleInternally,
+ expectedAsk: false,
+ },
+ {
+ description:
+ "Saving to disk when internal handling is the default *should* change prefs if checkbox is ticked.",
+ preDialogState: { preferredAction: handleInternally, alwaysAsk: false },
+ dialogActions(doc) {
+ let saveItem = doc.querySelector("#save");
+ saveItem.click();
+ ok(saveItem.selected, "The 'save' option should now be selected");
+ let checkbox = doc.querySelector("#rememberChoice");
+ checkbox.checked = true;
+ checkbox.doCommand();
+ },
+ expectTab: false,
+ expectLaunch: false,
+ expectedPreferredAction: saveToDisk,
+ expectedAsk: false,
+ },
+ {
+ description:
+ "Saving to disk when asking is the default should change persisted default.",
+ preDialogState: { preferredAction: handleInternally, alwaysAsk: true },
+ dialogActions(doc) {
+ let saveItem = doc.querySelector("#save");
+ saveItem.click();
+ ok(saveItem.selected, "The 'save' option should now be selected");
+ },
+ expectTab: false,
+ expectLaunch: false,
+ expectedPreferredAction: saveToDisk,
+ expectedAsk: true,
+ },
+ {
+ description:
+ "Opening externally when asking is the default should change persisted default.",
+ preDialogState: { preferredAction: handleInternally, alwaysAsk: true },
+ dialogActions(doc) {
+ let openItem = doc.querySelector("#open");
+ openItem.click();
+ ok(openItem.selected, "The 'save' option should now be selected");
+ },
+ expectTab: false,
+ expectLaunch: true,
+ expectedPreferredAction: useSystemDefault,
+ expectedAsk: true,
+ },
+];
+
+function ensureMIMEState({ preferredAction, alwaysAsk }) {
+ const mimeInfo = gMimeSvc.getFromTypeAndExtension("application/pdf", "pdf");
+ mimeInfo.preferredAction = preferredAction;
+ mimeInfo.alwaysAskBeforeHandling = alwaysAsk;
+ gHandlerSvc.store(mimeInfo);
+}
+
+/**
+ * Test that if we have PDFs set to handle internally, and the user chooses to
+ * do something else with it, we do not alter the saved state.
+ */
+add_task(async function test_check_saving_handler_choices() {
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+ for (let testCase of kTestCases) {
+ let file = "file_pdf_application_pdf.pdf";
+ info("Testing with " + file + "; " + testCase.description);
+ ensureMIMEState(testCase.preDialogState);
+
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + file
+ );
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+ let doc = dialogWindow.document;
+ let internalHandlerRadio = doc.querySelector("#handleInternally");
+
+ await waitForAcceptButtonToGetEnabled(doc);
+
+ ok(!internalHandlerRadio.hidden, "The option should be visible for PDF");
+ ok(
+ internalHandlerRadio.selected,
+ "The Firefox option should be selected by default"
+ );
+
+ const { expectTab, expectLaunch, description } = testCase;
+ // Prep to intercept things so we only see the results we want.
+ let tabOpenListener = ev => {
+ ok(
+ expectTab,
+ `A new tab should ${expectTab ? "" : "not "}be opened - ${description}`
+ );
+ BrowserTestUtils.removeTab(ev.target);
+ };
+ gBrowser.tabContainer.addEventListener("TabOpen", tabOpenListener);
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ let fileLaunched = PromiseUtils.defer();
+ DownloadIntegration.launchFile = () => {
+ ok(
+ expectLaunch,
+ `The file should ${
+ expectLaunch ? "" : "not "
+ }be launched with an external application - ${description}`
+ );
+ fileLaunched.resolve();
+ };
+ let downloadFinishedPromise = promiseDownloadFinished(publicList);
+
+ await testCase.dialogActions(doc);
+
+ let dialog = doc.querySelector("#unknownContentType");
+ dialog.acceptDialog();
+
+ let download = await downloadFinishedPromise;
+ if (expectLaunch) {
+ await fileLaunched.promise;
+ }
+ DownloadIntegration.launchFile = oldLaunchFile;
+ gBrowser.tabContainer.removeEventListener("TabOpen", tabOpenListener);
+
+ is(
+ (await publicList.getAll()).length,
+ 1,
+ "download should appear in public list"
+ );
+
+ // Check mime info:
+ const mimeInfo = gMimeSvc.getFromTypeAndExtension("application/pdf", "pdf");
+ gHandlerSvc.fillHandlerInfo(mimeInfo, "");
+ is(
+ mimeInfo.preferredAction,
+ testCase.expectedPreferredAction,
+ "preferredAction - " + description
+ );
+ is(
+ mimeInfo.alwaysAskBeforeHandling,
+ testCase.expectedAsk,
+ "alwaysAsk - " + description
+ );
+
+ BrowserTestUtils.removeTab(loadingTab);
+ await publicList.removeFinished();
+ if (download?.target.exists) {
+ try {
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {
+ /* ignore */
+ }
+ }
+ }
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js
new file mode 100644
index 0000000000..5f60e39bb1
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js
@@ -0,0 +1,398 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const CONTENT_HANDLING_URL =
+ "chrome://mozapps/content/handling/appChooser.xhtml";
+
+add_task(setupMailHandler);
+
+/**
+ * Check that if we open the protocol handler dialog from a subframe, we close
+ * it when closing the tab.
+ */
+add_task(async function test_closed_by_tab_closure() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + "file_nested_protocol_request.html"
+ );
+
+ // Wait for the window and then click the link.
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a:link",
+ {},
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+
+ let dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Dialog URL is as expected"
+ );
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+
+ info("Removing tab to close the dialog.");
+ gBrowser.removeTab(tab);
+ await dialogClosedPromise;
+ ok(!dialog._frame.contentWindow, "The dialog should have been closed.");
+});
+
+/**
+ * Check that if we open the protocol handler dialog from a subframe, we close
+ * it when navigating the tab to a non-same-origin URL.
+ */
+add_task(async function test_closed_by_tab_navigation() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + "file_nested_protocol_request.html"
+ );
+
+ // Wait for the window and then click the link.
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a:link",
+ {},
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+ let dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Dialog URL is as expected"
+ );
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+ info(
+ "Set up unload handler to ensure we don't break when the window global gets cleared"
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
+ content.addEventListener("unload", function() {});
+ });
+
+ info("Navigating tab to a different but same origin page.");
+ BrowserTestUtils.loadURI(tab.linkedBrowser, TEST_PATH);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, TEST_PATH);
+ ok(dialog._frame.contentWindow, "Dialog should stay open.");
+
+ // The use of weak references in various parts of the code means that we're
+ // susceptible to dropping crucial bits of our implementation on the floor,
+ // if they get GC'd, and then the test hangs.
+ // Do a bunch of GC/CC runs so that if we ever break, it's deterministic.
+ let numCycles = 3;
+ for (let i = 0; i < numCycles; i++) {
+ Cu.forceGC();
+ Cu.forceCC();
+ }
+
+ info("Now navigate to a cross-origin page.");
+ const CROSS_ORIGIN_TEST_PATH = TEST_PATH.replace(".com", ".org");
+ BrowserTestUtils.loadURI(tab.linkedBrowser, CROSS_ORIGIN_TEST_PATH);
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ CROSS_ORIGIN_TEST_PATH
+ );
+ await dialogClosedPromise;
+ ok(!dialog._frame.contentWindow, "The dialog should have been closed.");
+
+ // Avoid errors from aborted loads by waiting for it to finish.
+ await loadPromise;
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Check that we cannot open more than one of these dialogs.
+ */
+add_task(async function test_multiple_dialogs() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + "file_nested_protocol_request.html"
+ );
+
+ // Wait for the window and then click the link.
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a:link",
+ {},
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+ let dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Dialog URL is as expected"
+ );
+
+ // Navigate the parent frame:
+ await ContentTask.spawn(tab.linkedBrowser, [], () =>
+ content.eval("location.href = 'mailto:help@example.com'")
+ );
+
+ // Wait for a few ticks:
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 100));
+ // Check we only have one dialog
+
+ let tabDialogBox = gBrowser.getTabDialogBox(tab.linkedBrowser);
+ let dialogs = tabDialogBox
+ .getTabDialogManager()
+ ._dialogs.filter(d => d._openedURL == CONTENT_HANDLING_URL);
+
+ is(dialogs.length, 1, "Should only have 1 dialog open");
+
+ // Close the dialog:
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+ dialog.close();
+ dialog = await dialogClosedPromise;
+
+ ok(!dialog._openedURL, "The dialog should have been closed.");
+
+ // Then reopen the dialog again, to make sure we don't keep blocking:
+ dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a:link",
+ {},
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+ dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Second dialog URL is as expected"
+ );
+
+ dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+ info("Removing tab to close the dialog.");
+ gBrowser.removeTab(tab);
+ await dialogClosedPromise;
+ ok(!dialog._frame.contentWindow, "The dialog should have been closed again.");
+});
+
+/**
+ * Check that navigating invisible frames to external-proto URLs
+ * is handled correctly.
+ */
+add_task(async function invisible_iframes() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/"
+ );
+
+ // Ensure we notice the dialog opening:
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function() {
+ let frame = content.document.createElement("iframe");
+ frame.style.display = "none";
+ frame.src = "mailto:help@example.com";
+ content.document.body.append(frame);
+ });
+ let dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Dialog opens as expected for invisible iframe"
+ );
+ // Close the dialog:
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+ dialog.close();
+ await dialogClosedPromise;
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Check that nested iframes are handled correctly.
+ */
+add_task(async function nested_iframes() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/"
+ );
+
+ // Ensure we notice the dialog opening:
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+ let innerLoaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ true,
+ "https://example.org/"
+ );
+ info("Constructing top frame");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function() {
+ let frame = content.document.createElement("iframe");
+ frame.src = "https://example.org/"; // cross-origin frame.
+ content.document.body.prepend(frame);
+
+ content.eval(
+ `window.addEventListener("message", e => e.source.location = "mailto:help@example.com");`
+ );
+ });
+
+ await innerLoaded;
+ let parentBC = tab.linkedBrowser.browsingContext;
+
+ info("Creating innermost frame");
+ await SpecialPowers.spawn(parentBC.children[0], [], async function() {
+ let innerFrame = content.document.createElement("iframe");
+ let frameLoaded = ContentTaskUtils.waitForEvent(innerFrame, "load", true);
+ content.document.body.prepend(innerFrame);
+ await frameLoaded;
+ });
+
+ info("Posting event from innermost frame");
+ await SpecialPowers.spawn(
+ parentBC.children[0].children[0],
+ [],
+ async function() {
+ // Top browsing context needs reference to the innermost, which is cross origin.
+ content.eval("top.postMessage('hello', '*')");
+ }
+ );
+
+ let dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Dialog opens as expected for deeply nested cross-origin iframe"
+ );
+ // Close the dialog:
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+ dialog.close();
+ await dialogClosedPromise;
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_oop_iframe() {
+ const URI = `data:text/html,<div id="root"><iframe src="http://example.com/document-builder.sjs?html=<a href='mailto:help@example.com'>Mail it</a>"></iframe></div>`;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URI);
+
+ // Wait for the window and then click the link.
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a:link",
+ {},
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+
+ let dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Dialog URL is as expected"
+ );
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+
+ info("Removing tab to close the dialog.");
+ gBrowser.removeTab(tab);
+ await dialogClosedPromise;
+ ok(!dialog._frame.contentWindow, "The dialog should have been closed.");
+});
+
+/**
+ * Check that a cross-origin iframe can navigate the top frame
+ * to an external protocol.
+ */
+add_task(async function xorigin_iframe_can_navigate_top() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/"
+ );
+
+ // Ensure we notice the dialog opening:
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+ let innerLoaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ true,
+ "https://example.org/"
+ );
+ info("Constructing frame");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function() {
+ let frame = content.document.createElement("iframe");
+ frame.src = "https://example.org/"; // cross-origin frame.
+ content.document.body.prepend(frame);
+ });
+ await innerLoaded;
+
+ info("Navigating top bc from frame");
+ let parentBC = tab.linkedBrowser.browsingContext;
+ await SpecialPowers.spawn(parentBC.children[0], [], async function() {
+ content.eval("window.top.location.href = 'mailto:example@example.com';");
+ });
+
+ let dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Dialog opens as expected for navigating the top frame from an x-origin frame."
+ );
+ // Close the dialog:
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+ dialog.close();
+ await dialogClosedPromise;
+ gBrowser.removeTab(tab);
+});
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..7050616dc3
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js
@@ -0,0 +1,754 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.import(
+ "resource://testing-common/HandlerServiceTestUtils.jsm",
+ this
+);
+
+let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+
+// Testing multiple protocol / origin combinations takes long on debug.
+requestLongerTimeout(3);
+
+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
+);
+
+let testExtension;
+
+/**
+ * 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."
+ );
+}
+
+/**
+ * Open a test URL with the desired scheme.
+ * By default the load is triggered by the content principal of the browser.
+ * @param {MozBrowser} browser - Browser to load the test URL in.
+ * @param {string} scheme - Scheme of the test URL.
+ * @param {Object} [opts] - Options for the triggering principal.
+ * @param {nsIPrincipal} [opts.triggeringPrincipal] - Principal to trigger the
+ * load with. Defaults to the browsers content principal.
+ * @param {boolean} [opts.useNullPrincipal] - If true, we will trigger the load
+ * with a null principal.
+ * @param {boolean} [opts.useExtensionPrincipal] - If true, we will trigger the
+ * load with an extension.
+ * @param {boolean} [opts.omitTriggeringPrincipal] - If true, we will directly
+ * call the protocol handler dialogs without a principal.
+ */
+async function triggerOpenProto(
+ browser,
+ scheme,
+ {
+ triggeringPrincipal = browser.contentPrincipal,
+ useNullPrincipal = false,
+ useExtensionPrincipal = false,
+ omitTriggeringPrincipal = false,
+ } = {}
+) {
+ let uri = `${scheme}://test`;
+
+ if (useNullPrincipal) {
+ // Create and load iframe with data URI.
+ // This will be a null principal.
+ 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);
+ });
+ return;
+ }
+
+ if (useExtensionPrincipal) {
+ 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();
+ return;
+ }
+
+ if (omitTriggeringPrincipal) {
+ // Directly call ContentDispatchChooser without a triggering principal
+ 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
+ );
+ return;
+ }
+
+ info("Loading uri: " + uri);
+ browser.loadURI(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 {Object} [options.loadOptions] - Options for triggering the protocol
+ * load which causes the dialog to show.
+ * @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, loadOptions } = {}
+) {
+ 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 triggerOpenProto(browser, scheme, loadOptions);
+ 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,
+ hasChangeApp,
+ chooserIsNext,
+ actionCheckbox,
+ actionConfirm,
+ actionChangeApp,
+ } = permDialogOptions;
+
+ if (actionChangeApp) {
+ actionConfirm = false;
+ }
+
+ await testCheckbox(dialogEl, dialogType, {
+ hasCheckbox,
+ actionCheckbox,
+ });
+
+ // 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 (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");
+ }
+
+ // Clean up test extension if needed.
+ await testExtension?.unload();
+}
+
+/**
+ * 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 }
+) {
+ let checkbox = dialogEl.ownerDocument.getElementById("remember");
+ if (typeof hasCheckbox == "boolean") {
+ let hiddenEl;
+ if (dialogType == "permission") {
+ hiddenEl = checkbox.parentElement;
+ } else {
+ hiddenEl = checkbox;
+ }
+ is(
+ checkbox && !hiddenEl.hidden,
+ hasCheckbox,
+ "Dialog checkbox has correct visibility."
+ );
+ }
+
+ if (typeof hasCheckboxState == "boolean") {
+ is(checkbox.checked, hasCheckboxState, "Dialog checkbox has correct state");
+ }
+
+ if (typeof actionCheckbox == "boolean") {
+ checkbox.focus();
+ await EventUtils.synthesizeKey("VK_SPACE", undefined, dialogEl.ownerWindow);
+ }
+}
+
+/**
+ * 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_task(async function setup() {
+ 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 },
+ loadOptions: {
+ triggeringPrincipal: 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, {
+ loadOptions: {
+ useNullPrincipal: true,
+ },
+ 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, {
+ loadOptions: {
+ omitTriggeringPrincipal: true,
+ },
+ permDialogOptions: {
+ hasCheckbox: false,
+ chooserIsNext: true,
+ hasChangeApp: false,
+ actionConfirm: true,
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
+
+/**
+ * Tests that we skip the permission dialog for extension callers.
+ */
+add_task(async function test_extension_principal() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ loadOptions: {
+ useExtensionPrincipal: true,
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js b/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js
new file mode 100644
index 0000000000..b9ba4a7955
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_helperapp() {
+ // Set up the test infrastructure:
+ const kProt = "foopydoopydoo";
+ const extProtocolSvc = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ let handlerInfo = extProtocolSvc.getProtocolHandlerInfo(kProt);
+ if (handlerSvc.exists(handlerInfo)) {
+ handlerSvc.fillHandlerInfo(handlerInfo, "");
+ }
+ // Say we want to use a specific app:
+ handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
+ handlerInfo.alwaysAskBeforeHandling = false;
+
+ // Say it's us:
+ let selfFile = Services.dirsvc.get("XREExeF", Ci.nsIFile);
+ // Make sure it's the .app
+ if (AppConstants.platform == "macosx") {
+ while (
+ !selfFile.leafName.endsWith(".app") &&
+ !selfFile.leafName.endsWith(".app/")
+ ) {
+ selfFile = selfFile.parent;
+ }
+ }
+ let selfHandlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ selfHandlerApp.executable = selfFile;
+ handlerInfo.possibleApplicationHandlers.appendElement(selfHandlerApp);
+ handlerInfo.preferredApplicationHandler = selfHandlerApp;
+ handlerSvc.store(handlerInfo);
+
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ // Now, do some safety stubbing. If we do end up recursing we spawn
+ // infinite tabs. We definitely don't want that. Avoid it by stubbing
+ // our external URL handling bits:
+ let oldAddTab = gBrowser.addTab;
+ registerCleanupFunction(
+ () => (gBrowser.addTab = gBrowser.loadOneTab = oldAddTab)
+ );
+ let wrongThingHappenedPromise = new Promise(resolve => {
+ gBrowser.addTab = gBrowser.loadOneTab = function(aURI) {
+ ok(false, "Tried to open unexpected URL in a tab: " + aURI);
+ resolve(null);
+ // Pass a dummy object to avoid upsetting BrowserContentHandler -
+ // if it thinks opening the tab failed, it tries to open a window instead,
+ // which we can't prevent as easily, and at which point we still end up
+ // with runaway tabs.
+ return {};
+ };
+ });
+
+ let askedUserPromise = waitForProtocolAppChooserDialog(browser, true);
+
+ BrowserTestUtils.loadURI(browser, kProt + ":test");
+ let dialog = await Promise.race([
+ wrongThingHappenedPromise,
+ askedUserPromise,
+ ]);
+ ok(dialog, "Should have gotten a dialog");
+
+ let closePromise = waitForProtocolAppChooserDialog(browser, false);
+ dialog.close();
+ await closePromise;
+ askedUserPromise = null;
+ });
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js b/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js
new file mode 100644
index 0000000000..28bd50a120
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js
@@ -0,0 +1,61 @@
+add_task(async function() {
+ // create mocked objects
+ let launcher = createMockedObjects(true);
+
+ // open helper app dialog with mocked launcher
+ let dlg = await openHelperAppDialog(launcher);
+
+ let doc = dlg.document;
+ let dialogElement = doc.getElementById("unknownContentType");
+
+ // Set remember choice
+ ok(
+ !doc.getElementById("rememberChoice").checked,
+ "Remember choice checkbox should be not checked."
+ );
+ doc.getElementById("rememberChoice").checked = true;
+
+ // Make sure the mock handler information is not in nsIHandlerService
+ ok(
+ !gHandlerSvc.exists(launcher.MIMEInfo),
+ "Should not be in nsIHandlerService."
+ );
+
+ // close the dialog by pushing the ok button.
+ let dialogClosedPromise = BrowserTestUtils.windowClosed(dlg);
+ // Make sure the ok button is enabled, since the ok button might be disabled by
+ // EnableDelayHelper mechanism. Please refer the detailed
+ // https://searchfox.org/mozilla-central/source/toolkit/components/prompts/src/SharedPromptUtils.jsm#53
+ dialogElement.getButton("accept").disabled = false;
+ dialogElement.acceptDialog();
+ await dialogClosedPromise;
+
+ // check the mocked handler information is saved in nsIHandlerService
+ ok(gHandlerSvc.exists(launcher.MIMEInfo), "Should be in nsIHandlerService.");
+ // check the extension.
+ var mimeType = gHandlerSvc.getTypeFromExtension("abc");
+ is(mimeType, launcher.MIMEInfo.type, "Got correct mime type.");
+ for (let handlerInfo of gHandlerSvc.enumerate()) {
+ if (handlerInfo.type == launcher.MIMEInfo.type) {
+ // check the alwaysAskBeforeHandling
+ ok(
+ !handlerInfo.alwaysAskBeforeHandling,
+ "Should turn off the always ask."
+ );
+ // check the preferredApplicationHandler
+ ok(
+ handlerInfo.preferredApplicationHandler.equals(
+ launcher.MIMEInfo.preferredApplicationHandler
+ ),
+ "Should be equal to the mockedHandlerApp."
+ );
+ // check the perferredAction
+ is(
+ handlerInfo.preferredAction,
+ launcher.MIMEInfo.preferredAction,
+ "Should be equal to Ci.nsIHandlerInfo.useHelperApp."
+ );
+ break;
+ }
+ }
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js b/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js
new file mode 100644
index 0000000000..66f461834c
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js
@@ -0,0 +1,124 @@
+let testURL =
+ "https://example.com/browser/" +
+ "uriloader/exthandler/tests/mochitest/protocolHandler.html";
+
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.external_protocol_requires_permission", false]],
+ });
+
+ // Load a page registering a protocol handler.
+ let browser = gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURI(browser, testURL);
+ await BrowserTestUtils.browserLoaded(browser, false, testURL);
+
+ // Register the protocol handler by clicking the notificationbar button.
+ let notificationValue = "Protocol Registration: web+testprotocol";
+ let getNotification = () =>
+ gBrowser.getNotificationBox().getNotificationWithValue(notificationValue);
+ await BrowserTestUtils.waitForCondition(getNotification);
+ let notification = getNotification();
+ let button = notification.querySelector("button");
+ ok(button, "got registration button");
+ button.click();
+
+ // Set the new handler as default.
+ const protoSvc = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ let protoInfo = protoSvc.getProtocolHandlerInfo("web+testprotocol");
+ is(
+ protoInfo.preferredAction,
+ protoInfo.useHelperApp,
+ "using a helper application is the preferred action"
+ );
+ ok(!protoInfo.preferredApplicationHandler, "no preferred handler is set");
+ let handlers = protoInfo.possibleApplicationHandlers;
+ is(1, handlers.length, "only one handler registered for web+testprotocol");
+ let handler = handlers.queryElementAt(0, Ci.nsIHandlerApp);
+ ok(handler instanceof Ci.nsIWebHandlerApp, "the handler is a web handler");
+ is(
+ handler.uriTemplate,
+ "https://example.com/foobar?uri=%s",
+ "correct url template"
+ );
+ protoInfo.preferredApplicationHandler = handler;
+ protoInfo.alwaysAskBeforeHandling = false;
+ const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ handlerSvc.store(protoInfo);
+
+ const expectedURL =
+ "https://example.com/foobar?uri=web%2Btestprotocol%3Atest";
+
+ // Create a framed link:
+ await SpecialPowers.spawn(browser, [], async function() {
+ let iframe = content.document.createElement("iframe");
+ iframe.src = `data:text/html,<a href="web+testprotocol:test">Click me</a>`;
+ content.document.body.append(iframe);
+ // Can't return this promise because it resolves to the event object.
+ await ContentTaskUtils.waitForEvent(iframe, "load");
+ iframe.contentDocument.querySelector("a").click();
+ });
+ let kidContext = browser.browsingContext.children[0];
+ await TestUtils.waitForCondition(() => {
+ let spec = kidContext.currentWindowGlobal?.documentURI?.spec || "";
+ return spec == expectedURL;
+ });
+ is(
+ kidContext.currentWindowGlobal.documentURI.spec,
+ expectedURL,
+ "Should load in frame."
+ );
+
+ // Middle-click a testprotocol link and check the new tab is correct
+ let link = "#link";
+
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL);
+ await BrowserTestUtils.synthesizeMouseAtCenter(link, { button: 1 }, browser);
+ let tab = await promiseTabOpened;
+ gBrowser.selectedTab = tab;
+ is(
+ gURLBar.value,
+ expectedURL,
+ "the expected URL is displayed in the location bar"
+ );
+ BrowserTestUtils.removeTab(tab);
+
+ // Shift-click the testprotocol link and check the new window.
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow({
+ url: expectedURL,
+ });
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ link,
+ { shiftKey: true },
+ browser
+ );
+ let win = await newWindowPromise;
+ await BrowserTestUtils.waitForCondition(
+ () => win.gBrowser.currentURI.spec == expectedURL
+ );
+ is(
+ win.gURLBar.value,
+ expectedURL,
+ "the expected URL is displayed in the location bar"
+ );
+ await BrowserTestUtils.closeWindow(win);
+
+ // Click the testprotocol link and check the url in the current tab.
+ let loadPromise = BrowserTestUtils.browserLoaded(browser);
+ await BrowserTestUtils.synthesizeMouseAtCenter(link, {}, browser);
+ await loadPromise;
+ await BrowserTestUtils.waitForCondition(() => gURLBar.value != testURL);
+ is(
+ gURLBar.value,
+ expectedURL,
+ "the expected URL is displayed in the location bar"
+ );
+
+ // Cleanup.
+ protoInfo.preferredApplicationHandler = null;
+ handlers.removeElementAt(0);
+ handlerSvc.store(protoInfo);
+});
diff --git a/uriloader/exthandler/tests/mochitest/download.bin b/uriloader/exthandler/tests/mochitest/download.bin
new file mode 100644
index 0000000000..0e4b0c7bae
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/download.bin
@@ -0,0 +1 @@
+abc123
diff --git a/uriloader/exthandler/tests/mochitest/download.sjs b/uriloader/exthandler/tests/mochitest/download.sjs
new file mode 100644
index 0000000000..bee7bd7015
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/download.sjs
@@ -0,0 +1,38 @@
+"use strict";
+
+Cu.import("resource://gre/modules/Timer.jsm");
+
+function actuallyHandleRequest(req, res) {
+ res.setHeader("Content-Type", "application/octet-stream", false);
+ res.write("abc123");
+ res.finish();
+}
+
+function handleRequest(req, res) {
+ if (req.queryString.includes('finish')) {
+ res.write("OK");
+ let downloadReq = null;
+ getObjectState("downloadReq", o => { downloadReq = o });
+ // Two possibilities: either the download request has already reached us, or not.
+ if (downloadReq) {
+ downloadReq.wrappedJSObject.callback();
+ } else {
+ // Set a variable to allow the request to complete immediately:
+ setState("finishReq", "true");
+ }
+ } else if (req.queryString.includes('reset')) {
+ res.write("OK");
+ setObjectState("downloadReq", null);
+ setState("finishReq", "false");
+ } else {
+ res.processAsync();
+ if (getState("finishReq") === "true") {
+ actuallyHandleRequest(req, res);
+ } else {
+ let o = {callback() { actuallyHandleRequest(req, res) }};
+ o.wrappedJSObject = o;
+ o.QueryInterface = () => o;
+ setObjectState("downloadReq", o);
+ }
+ }
+}
diff --git a/uriloader/exthandler/tests/mochitest/download_page.html b/uriloader/exthandler/tests/mochitest/download_page.html
new file mode 100644
index 0000000000..5a264888fa
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/download_page.html
@@ -0,0 +1,22 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset=UTF-8>
+ <title>Test page for link clicking</title>
+ <script type="text/javascript">
+ function launch_download(extra) {
+ window.open("download.sjs", "_blank", "height=100,width=100" + extra);
+ }
+ </script>
+</head>
+<body>
+ <a href="download.bin" id="regular_load">regular load</a>
+ <a href="download.bin" id="target_blank" target="_blank" rel="opener">target blank</a>
+ <a href="#" onclick="launch_download(''); return false" id="new_window">new window</a>
+ <a href="#" onclick="window.open('download_page.html?newwin'); return false" id="open_in_new_tab">click to reopen</a>
+ <a href="download.bin" id="target_blank_no_opener" rel="noopener" target="_blank">target blank (noopener)</a>
+ <a href="#" onclick="window.open('download.bin', '_blank', 'noopener'); return false" id="open_in_new_tab_no_opener">click to reopen (noopener)</a>
+ <a href="#" onclick="launch_download(',noopener'); return false" id="new_window_no_opener">new window (noopener)</a>
+</body>
diff --git a/uriloader/exthandler/tests/mochitest/file_as.exe b/uriloader/exthandler/tests/mochitest/file_as.exe
new file mode 100644
index 0000000000..f2f5ab47f3
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_as.exe
@@ -0,0 +1 @@
+Not actually an executable... but let's pretend!
diff --git a/uriloader/exthandler/tests/mochitest/file_as.exe^headers^ b/uriloader/exthandler/tests/mochitest/file_as.exe^headers^
new file mode 100644
index 0000000000..89f22e30be
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_as.exe^headers^
@@ -0,0 +1,2 @@
+Content-Type: binary/octet-stream
+Content-Disposition: attachment
diff --git a/uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html b/uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html
new file mode 100644
index 0000000000..eb2fb74441
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html
@@ -0,0 +1 @@
+<iframe src="mailto:foo@bar.com"></iframe>
diff --git a/uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html b/uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html
new file mode 100644
index 0000000000..b1bb863f89
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html
@@ -0,0 +1 @@
+<iframe srcdoc="<a href='mailto:help@example.com'>Mail someone</a>"></iframe>
diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf
diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^
new file mode 100644
index 0000000000..d1d59b9754
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^
@@ -0,0 +1,2 @@
+content-disposition: attachment; filename=file_pdf_application_pdf.pdf; filename*=UTF-8''file_pdf_application_pdf.pdf
+content-type: application/pdf
diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf
diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^
new file mode 100644
index 0000000000..157c0e0943
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^
@@ -0,0 +1,2 @@
+content-disposition: attachment; filename=file_pdf_application_unknown.pdf; filename*=UTF-8''file_pdf_application_unknown.pdf
+content-type: application/unknown
diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf
diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^
new file mode 100644
index 0000000000..6358f54f48
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^
@@ -0,0 +1,2 @@
+Content-Disposition: attachment; filename="file_pdf_binary_octet_stream.pdf"; filename*=UTF-8''file_pdf_binary_octet_stream.pdf
+Content-Type: binary/octet-stream
diff --git a/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt
diff --git a/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^ b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^
new file mode 100644
index 0000000000..37823166a4
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^
@@ -0,0 +1,2 @@
+Content-Disposition: attachment; filename=file_text_attachment_test.txt
+Content-Type: text/plain
diff --git a/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png
new file mode 100644
index 0000000000..743292dc6f
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png
Binary files differ
diff --git a/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^ b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^
new file mode 100644
index 0000000000..06e0cd957f
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^
@@ -0,0 +1,2 @@
+Content-Disposition: inline; filename=file_with%40%40funny_name.png
+Content-Type: image/png
diff --git a/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm
new file mode 100644
index 0000000000..7bc738b8b4
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm
Binary files differ
diff --git a/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^ b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^
new file mode 100644
index 0000000000..b77e9d3687
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^
@@ -0,0 +1,2 @@
+Content-Disposition: inline; filename=file_with%5Bfunny_name.webm
+Content-Type: video/webm
diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml
new file mode 100644
index 0000000000..3a5792586a
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml
@@ -0,0 +1,4 @@
+<?xml version = "1.0" encoding = "utf-8"?>
+
+<something>
+</something>
diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^
new file mode 100644
index 0000000000..5bdc4448e8
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^
@@ -0,0 +1,2 @@
+Content-Disposition: attachment
+Content-Type: binary/octet-stream
diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml
new file mode 100644
index 0000000000..3a5792586a
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml
@@ -0,0 +1,4 @@
+<?xml version = "1.0" encoding = "utf-8"?>
+
+<something>
+</something>
diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^
new file mode 100644
index 0000000000..ac0355d976
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^
@@ -0,0 +1,2 @@
+Content-Disposition: attachment; filename=file_xml_attachment_test.xml
+Content-Type: text/xml
diff --git a/uriloader/exthandler/tests/mochitest/handlerApp.xhtml b/uriloader/exthandler/tests/mochitest/handlerApp.xhtml
new file mode 100644
index 0000000000..e519e80029
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/handlerApp.xhtml
@@ -0,0 +1,28 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Pseudo Web Handler App</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="onLoad()">
+Pseudo Web Handler App
+
+<script class="testbody" type="text/javascript">
+<![CDATA[
+function onLoad() {
+ // if we have a window.opener, this must be the windowContext
+ // instance of this test. check that we got the URI right and clean up.
+ if (window.opener) {
+ window.opener.is(location.search,
+ "?uri=" + encodeURIComponent(window.opener.testURI),
+ "uri passed to web-handler app");
+ window.opener.SimpleTest.finish();
+ }
+
+ window.close();
+}
+]]>
+</script>
+
+</body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/handlerApps.js b/uriloader/exthandler/tests/mochitest/handlerApps.js
new file mode 100644
index 0000000000..aa841f13be
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/handlerApps.js
@@ -0,0 +1,118 @@
+/* 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/. */
+
+// handlerApp.xhtml grabs this for verification purposes via window.opener
+var testURI = "webcal://127.0.0.1/rheeeeet.html";
+
+const Cc = SpecialPowers.Cc;
+
+function test() {
+ // set up the web handler object
+ var webHandler = Cc[
+ "@mozilla.org/uriloader/web-handler-app;1"
+ ].createInstance(SpecialPowers.Ci.nsIWebHandlerApp);
+ webHandler.name = "Test Web Handler App";
+ webHandler.uriTemplate =
+ "https://example.com/tests/uriloader/exthandler/tests/mochitest/" +
+ "handlerApp.xhtml?uri=%s";
+
+ // set up the uri to test with
+ /* eslint-disable mozilla/use-services */
+
+ var ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ SpecialPowers.Ci.nsIIOService
+ );
+
+ var uri = ioService.newURI(testURI);
+
+ // create a window, and launch the handler in it
+ var newWindow = window.open("", "handlerWindow", "height=300,width=300");
+ var windowContext = SpecialPowers.wrap(newWindow).docShell;
+
+ webHandler.launchWithURI(uri, windowContext);
+
+ // if we get this far without an exception, we've at least partly passed
+ // (remaining check in handlerApp.xhtml)
+ ok(true, "webHandler launchWithURI (existing window/tab) started");
+
+ // make the web browser launch in its own window/tab
+ webHandler.launchWithURI(uri);
+
+ // if we get this far without an exception, we've passed
+ ok(true, "webHandler launchWithURI (new window/tab) test started");
+
+ // set up the local handler object
+ var localHandler = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(SpecialPowers.Ci.nsILocalHandlerApp);
+ localHandler.name = "Test Local Handler App";
+
+ // get a local app that we know will be there and do something sane
+ /* eslint-disable mozilla/use-services */
+
+ var osString = Cc["@mozilla.org/xre/app-info;1"].getService(
+ SpecialPowers.Ci.nsIXULRuntime
+ ).OS;
+
+ var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(
+ SpecialPowers.Ci.nsIDirectoryServiceProvider
+ );
+ if (osString == "WINNT") {
+ var windowsDir = dirSvc.getFile("WinD", {});
+ var exe = windowsDir.clone().QueryInterface(SpecialPowers.Ci.nsIFile);
+ exe.appendRelativePath("SYSTEM32\\HOSTNAME.EXE");
+ } else if (osString == "Darwin") {
+ var localAppsDir = dirSvc.getFile("LocApp", {});
+ exe = localAppsDir.clone();
+ exe.append("iCal.app"); // lingers after the tests finish, but this seems
+ // seems better than explicitly killing it, since
+ // developers who run the tests locally may well
+ // information in their running copy of iCal
+
+ if (navigator.userAgent.match(/ SeaMonkey\//)) {
+ // SeaMonkey tinderboxes don't like to have iCal lingering (and focused)
+ // on next test suite run(s).
+ todo(false, "On SeaMonkey, testing OS X as generic Unix. (Bug 749872)");
+
+ // assume a generic UNIX variant
+ exe = Cc["@mozilla.org/file/local;1"].createInstance(
+ SpecialPowers.Ci.nsIFile
+ );
+ exe.initWithPath("/bin/echo");
+ }
+ } else {
+ // assume a generic UNIX variant
+ exe = Cc["@mozilla.org/file/local;1"].createInstance(
+ SpecialPowers.Ci.nsIFile
+ );
+ exe.initWithPath("/bin/echo");
+ }
+
+ localHandler.executable = exe;
+ localHandler.launchWithURI(ioService.newURI(testURI));
+
+ // if we get this far without an exception, we've passed
+ ok(true, "localHandler launchWithURI test");
+
+ // if we ever decide that killing iCal is the right thing to do, change
+ // the if statement below from "NOTDarwin" to "Darwin"
+ if (osString == "NOTDarwin") {
+ var killall = Cc["@mozilla.org/file/local;1"].createInstance(
+ SpecialPowers.Ci.nsIFile
+ );
+ killall.initWithPath("/usr/bin/killall");
+
+ var process = Cc["@mozilla.org/process/util;1"].createInstance(
+ SpecialPowers.Ci.nsIProcess
+ );
+ process.init(killall);
+
+ var args = ["iCal"];
+ process.run(false, args, args.length);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+test();
diff --git a/uriloader/exthandler/tests/mochitest/head.js b/uriloader/exthandler/tests/mochitest/head.js
new file mode 100644
index 0000000000..3b0d8a4072
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/head.js
@@ -0,0 +1,277 @@
+var { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
+var { HandlerServiceTestUtils } = ChromeUtils.import(
+ "resource://testing-common/HandlerServiceTestUtils.jsm"
+);
+
+var gMimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+var gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+
+function createMockedHandlerApp() {
+ // Mock the executable
+ let mockedExecutable = FileUtils.getFile("TmpD", ["mockedExecutable"]);
+ if (!mockedExecutable.exists()) {
+ mockedExecutable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o755);
+ }
+
+ // Mock the handler app
+ let mockedHandlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ mockedHandlerApp.executable = mockedExecutable;
+ mockedHandlerApp.detailedDescription = "Mocked handler app";
+
+ registerCleanupFunction(function() {
+ // remove the mocked executable from disk.
+ if (mockedExecutable.exists()) {
+ mockedExecutable.remove(true);
+ }
+ });
+
+ return mockedHandlerApp;
+}
+
+function createMockedObjects(createHandlerApp) {
+ // Mock the mime info
+ let internalMockedMIME = gMimeSvc.getFromTypeAndExtension(
+ "text/x-test-handler",
+ null
+ );
+ internalMockedMIME.alwaysAskBeforeHandling = true;
+ internalMockedMIME.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
+ internalMockedMIME.appendExtension("abc");
+ if (createHandlerApp) {
+ let mockedHandlerApp = createMockedHandlerApp();
+ internalMockedMIME.description = mockedHandlerApp.detailedDescription;
+ internalMockedMIME.possibleApplicationHandlers.appendElement(
+ mockedHandlerApp
+ );
+ internalMockedMIME.preferredApplicationHandler = mockedHandlerApp;
+ }
+
+ // Proxy for the mocked MIME info for faking the read-only attributes
+ let mockedMIME = new Proxy(internalMockedMIME, {
+ get(target, property) {
+ switch (property) {
+ case "hasDefaultHandler":
+ return true;
+ case "defaultDescription":
+ return "Default description";
+ default:
+ return target[property];
+ }
+ },
+ });
+
+ // Mock the launcher:
+ let mockedLauncher = {
+ MIMEInfo: mockedMIME,
+ source: Services.io.newURI("http://www.mozilla.org/"),
+ suggestedFileName: "test_download_dialog.abc",
+ targetFileIsExecutable: false,
+ saveToDisk() {},
+ cancel() {},
+ launchWithApplication() {},
+ setWebProgressListener() {},
+ saveDestinationAvailable() {},
+ contentLength: 42,
+ targetFile: null, // never read
+ // PRTime is microseconds since epoch, Date.now() returns milliseconds:
+ timeDownloadStarted: Date.now() * 1000,
+ QueryInterface: ChromeUtils.generateQI([
+ "nsICancelable",
+ "nsIHelperAppLauncher",
+ ]),
+ };
+
+ registerCleanupFunction(function() {
+ // remove the mocked mime info from database.
+ let mockHandlerInfo = gMimeSvc.getFromTypeAndExtension(
+ "text/x-test-handler",
+ null
+ );
+ if (gHandlerSvc.exists(mockHandlerInfo)) {
+ gHandlerSvc.remove(mockHandlerInfo);
+ }
+ });
+
+ return mockedLauncher;
+}
+
+async function openHelperAppDialog(launcher) {
+ let helperAppDialog = Cc[
+ "@mozilla.org/helperapplauncherdialog;1"
+ ].createInstance(Ci.nsIHelperAppLauncherDialog);
+
+ let helperAppDialogShownPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ try {
+ helperAppDialog.show(launcher, window, "foopy");
+ } catch (ex) {
+ ok(
+ false,
+ "Trying to show unknownContentType.xhtml failed with exception: " + ex
+ );
+ Cu.reportError(ex);
+ }
+ let dlg = await helperAppDialogShownPromise;
+
+ is(
+ dlg.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Got correct dialog"
+ );
+
+ return dlg;
+}
+
+async function waitForSubDialog(browser, url, state) {
+ let eventStr = state ? "dialogopen" : "dialogclose";
+
+ let tabDialogBox = gBrowser.getTabDialogBox(browser);
+ let dialogStack = tabDialogBox.getTabDialogManager()._dialogStack;
+
+ let checkFn;
+
+ if (state) {
+ checkFn = dialogEvent => dialogEvent.detail.dialog?._openedURL == url;
+ }
+
+ let event = await BrowserTestUtils.waitForEvent(
+ dialogStack,
+ eventStr,
+ true,
+ checkFn
+ );
+
+ let { dialog } = event.detail;
+
+ // If the dialog is closing wait for it to be fully closed before resolving
+ if (!state) {
+ await dialog._closingPromise;
+ }
+
+ return event.detail.dialog;
+}
+
+/**
+ * Wait for protocol permission dialog open/close.
+ * @param {MozBrowser} browser - Browser element the dialog belongs to.
+ * @param {boolean} state - true: dialog open, false: dialog close
+ * @returns {Promise<SubDialog>} - Returns a promise which resolves with the
+ * SubDialog object of the dialog which closed or opened.
+ */
+async function waitForProtocolPermissionDialog(browser, state) {
+ return waitForSubDialog(
+ browser,
+ "chrome://mozapps/content/handling/permissionDialog.xhtml",
+ state
+ );
+}
+
+/**
+ * Wait for protocol app chooser dialog open/close.
+ * @param {MozBrowser} browser - Browser element the dialog belongs to.
+ * @param {boolean} state - true: dialog open, false: dialog close
+ * @returns {Promise<SubDialog>} - Returns a promise which resolves with the
+ * SubDialog object of the dialog which closed or opened.
+ */
+async function waitForProtocolAppChooserDialog(browser, state) {
+ return waitForSubDialog(
+ browser,
+ "chrome://mozapps/content/handling/appChooser.xhtml",
+ state
+ );
+}
+
+async function promiseDownloadFinished(list) {
+ return new Promise(resolve => {
+ list.addView({
+ onDownloadChanged(download) {
+ info("Download changed!");
+ if (download.succeeded || download.error) {
+ info("Download succeeded or errored");
+ list.removeView(this);
+ resolve(download);
+ }
+ },
+ });
+ });
+}
+
+function setupMailHandler() {
+ let mailHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto");
+ let gOldMailHandlers = [];
+
+ // Remove extant web handlers because they have icons that
+ // we fetch from the web, which isn't allowed in tests.
+ let handlers = mailHandlerInfo.possibleApplicationHandlers;
+ for (let i = handlers.Count() - 1; i >= 0; i--) {
+ try {
+ let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp);
+ gOldMailHandlers.push(handler);
+ // If we get here, this is a web handler app. Remove it:
+ handlers.removeElementAt(i);
+ } catch (ex) {}
+ }
+
+ let previousHandling = mailHandlerInfo.alwaysAskBeforeHandling;
+ mailHandlerInfo.alwaysAskBeforeHandling = true;
+
+ // Create a dummy web mail handler so we always know the mailto: protocol.
+ // Without this, the test fails on VMs without a default mailto: handler,
+ // because no dialog is ever shown, as we ignore subframe navigations to
+ // protocols that cannot be handled.
+ let dummy = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance(
+ Ci.nsIWebHandlerApp
+ );
+ dummy.name = "Handler 1";
+ dummy.uriTemplate = "https://example.com/first/%s";
+ mailHandlerInfo.possibleApplicationHandlers.appendElement(dummy);
+
+ gHandlerSvc.store(mailHandlerInfo);
+ registerCleanupFunction(() => {
+ // Re-add the original protocol handlers:
+ let mailHandlers = mailHandlerInfo.possibleApplicationHandlers;
+ for (let i = handlers.Count() - 1; i >= 0; i--) {
+ try {
+ // See if this is a web handler. If it is, it'll throw, otherwise,
+ // we will remove it.
+ mailHandlers.queryElementAt(i, Ci.nsIWebHandlerApp);
+ mailHandlers.removeElementAt(i);
+ } catch (ex) {}
+ }
+ for (let h of gOldMailHandlers) {
+ mailHandlers.appendElement(h);
+ }
+ mailHandlerInfo.alwaysAskBeforeHandling = previousHandling;
+ gHandlerSvc.store(mailHandlerInfo);
+ });
+}
+
+let gDownloadDir;
+
+async function setDownloadDir() {
+ let tmpDir = await PathUtils.getTempDir();
+ tmpDir = PathUtils.join(
+ tmpDir,
+ "testsavedir" + Math.floor(Math.random() * 2 ** 32)
+ );
+ // Create this dir if it doesn't exist (ignores existing dirs)
+ await IOUtils.makeDirectory(tmpDir);
+ registerCleanupFunction(async function() {
+ try {
+ await IOUtils.remove(tmpDir, { recursive: true });
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ });
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setCharPref("browser.download.dir", tmpDir);
+ return tmpDir;
+}
+
+add_task(async function test_common_initialize() {
+ gDownloadDir = await setDownloadDir();
+ Services.prefs.setCharPref("browser.download.loglevel", "Debug");
+});
diff --git a/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs b/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs
new file mode 100644
index 0000000000..d12e2904d9
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs
@@ -0,0 +1,14 @@
+/* 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/. */
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ if (!request.queryString.match(/^name=/))
+ return;
+ var name = decodeURIComponent(request.queryString.substring(5));
+
+ response.setHeader("Content-Type", "image/png; name=\"" + name + "\"");
+ response.setHeader("Content-Disposition", "attachment; filename=\"" + name + "\"");
+}
diff --git a/uriloader/exthandler/tests/mochitest/mochitest.ini b/uriloader/exthandler/tests/mochitest/mochitest.ini
new file mode 100644
index 0000000000..be3b6bb37e
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/mochitest.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+support-files =
+ handlerApp.xhtml
+ handlerApps.js
+
+[test_handlerApps.xhtml]
+skip-if = (toolkit == 'android' || os == 'mac') || e10s # OS X: bug 786938
+scheme = https
+[test_invalidCharFileExtension.xhtml]
+skip-if = toolkit == 'android' && !is_fennec # Bug 1525959
+support-files =
+ HelperAppLauncherDialog_chromeScript.js
+ invalidCharFileExtension.sjs
+[test_nullCharFile.xhtml]
+skip-if = toolkit == 'android' && !is_fennec # Bug 1525959
+support-files =
+ HelperAppLauncherDialog_chromeScript.js
+[test_unknown_ext_protocol_handlers.html]
+[test_unsafeBidiChars.xhtml]
+skip-if = toolkit == 'android' && !is_fennec # Bug 1525959
+support-files =
+ HelperAppLauncherDialog_chromeScript.js
+ unsafeBidiFileName.sjs
diff --git a/uriloader/exthandler/tests/mochitest/protocolHandler.html b/uriloader/exthandler/tests/mochitest/protocolHandler.html
new file mode 100644
index 0000000000..eff8a53aab
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/protocolHandler.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Protocol handler</title>
+ <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+ <meta content="utf-8" http-equiv="encoding">
+ </head>
+ <body>
+ <script type="text/javascript">
+ navigator.registerProtocolHandler("web+testprotocol",
+ "https://example.com/foobar?uri=%s",
+ "Test Protocol");
+ </script>
+ <a id="link" href="web+testprotocol:test">testprotocol link</a>
+ </body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/test_handlerApps.xhtml b/uriloader/exthandler/tests/mochitest/test_handlerApps.xhtml
new file mode 100644
index 0000000000..d6166fd270
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/test_handlerApps.xhtml
@@ -0,0 +1,11 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Handler Apps </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="handlerApps.js"/>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+</body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml b/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml
new file mode 100644
index 0000000000..4ee1a6a1c1
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml
@@ -0,0 +1,47 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Handling of unsafe bidi chars</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<iframe id="test"></iframe>
+<script type="text/javascript">
+var tests = [
+ ["test.png:large", "test.png"],
+ ["test.png/large", "test.png"],
+ [":test.png::large:", "test.png"],
+];
+
+add_task(async function() {
+ let url = SimpleTest.getTestFileURL("HelperAppLauncherDialog_chromeScript.js");
+ let chromeScript = SpecialPowers.loadChromeScript(url);
+
+ for (let [name, expected] of tests) {
+ let promiseName = new Promise(function(resolve) {
+ chromeScript.addMessageListener("suggestedFileName",
+ function listener(data) {
+ chromeScript.removeMessageListener("suggestedFileName", listener);
+ resolve(data);
+ });
+ });
+ document.getElementById("test").src =
+ "invalidCharFileExtension.sjs?name=" + encodeURIComponent(name);
+ is((await promiseName), expected, "got the expected sanitized name");
+ }
+
+ let promise = new Promise(function(resolve) {
+ chromeScript.addMessageListener("unregistered", function listener() {
+ chromeScript.removeMessageListener("unregistered", listener);
+ resolve();
+ });
+ });
+ chromeScript.sendAsyncMessage("unregister");
+ await promise;
+
+ chromeScript.destroy();
+});
+</script>
+</body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml b/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml
new file mode 100644
index 0000000000..9bb1140718
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml
@@ -0,0 +1,49 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Handling of null char</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<iframe id="test"></iframe>
+<script type="text/javascript">
+var tests = [
+ ["test.html\u0000.png", "test.html_.png"],
+ ["test.html.\u0000png", "test.html._png"],
+];
+
+add_task(async function() {
+ let url = SimpleTest.getTestFileURL("HelperAppLauncherDialog_chromeScript.js");
+ let chromeScript = SpecialPowers.loadChromeScript(url);
+
+ for (let [name, expected] of tests) {
+ let promiseName = new Promise(function(resolve) {
+ chromeScript.addMessageListener("suggestedFileName",
+ function listener(data) {
+ chromeScript.removeMessageListener("suggestedFileName", listener);
+ resolve(data);
+ });
+ });
+ const a = document.createElement('a');
+ // Pass an unknown mimetype so we don't "correct" the extension:
+ a.href = "data:application/baconizer;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==";
+ a.download = name;
+ a.dispatchEvent(new MouseEvent('click'));
+ is((await promiseName), expected, "got the expected sanitized name");
+ }
+
+ let promise = new Promise(function(resolve) {
+ chromeScript.addMessageListener("unregistered", function listener() {
+ chromeScript.removeMessageListener("unregistered", listener);
+ resolve();
+ });
+ });
+ chromeScript.sendAsyncMessage("unregister");
+ await promise;
+
+ chromeScript.destroy();
+});
+</script>
+</body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html b/uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html
new file mode 100644
index 0000000000..f8727db605
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for no error reporting for unknown external protocols</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe id="testFrame"></iframe>
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+window.onload = () => {
+ let testFrame = document.getElementById("testFrame");
+
+ try {
+ testFrame.contentWindow.location.href = "unknownextproto:";
+ ok(true, "There is no error reporting for unknown external protocol navigation.");
+ } catch (e) {
+ ok(false, "There should be no error reporting for unknown external protocol navigation.");
+ }
+
+ SimpleTest.finish();
+};
+</script>
+</body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml
new file mode 100644
index 0000000000..4f62b32d99
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml
@@ -0,0 +1,72 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Handling of unsafe bidi chars</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<iframe id="test"></iframe>
+<script type="text/javascript">
+var unsafeBidiChars = [
+ "\xe2\x80\xaa", // LRE
+ "\xe2\x80\xab", // RLE
+ "\xe2\x80\xac", // PDF
+ "\xe2\x80\xad", // LRO
+ "\xe2\x80\xae", // RLO
+];
+
+var tests = [
+ "{1}.test",
+ "{1}File.test",
+ "Fi{1}le.test",
+ "File{1}.test",
+ "File.{1}test",
+ "File.te{1}st",
+ "File.test{1}",
+ "File.{1}",
+];
+
+function replace(name, x) {
+ return name.replace(/\{1\}/, x);
+}
+
+function sanitize(name) {
+ return replace(name, "_");
+}
+
+add_task(async function() {
+ let url = SimpleTest.getTestFileURL("HelperAppLauncherDialog_chromeScript.js");
+ let chromeScript = SpecialPowers.loadChromeScript(url);
+
+ for (let test of tests) {
+ for (let char of unsafeBidiChars) {
+ let promiseName = new Promise(function(resolve) {
+ chromeScript.addMessageListener("suggestedFileName",
+ function listener(data) {
+ chromeScript.removeMessageListener("suggestedFileName", listener);
+ resolve(data);
+ });
+ });
+ let name = replace(test, char);
+ let expected = sanitize(test);
+ document.getElementById("test").src =
+ "unsafeBidiFileName.sjs?name=" + encodeURIComponent(name);
+ is((await promiseName), expected, "got the expected sanitized name");
+ }
+ }
+
+ let promise = new Promise(function(resolve) {
+ chromeScript.addMessageListener("unregistered", function listener() {
+ chromeScript.removeMessageListener("unregistered", listener);
+ resolve();
+ });
+ });
+ chromeScript.sendAsyncMessage("unregister");
+ await promise;
+
+ chromeScript.destroy();
+});
+</script>
+</body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs b/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs
new file mode 100644
index 0000000000..48301be5b4
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs
@@ -0,0 +1,14 @@
+/* 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/. */
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ if (!request.queryString.match(/^name=/))
+ return;
+ var name = decodeURIComponent(request.queryString.substring(5));
+
+ response.setHeader("Content-Type", "application/octet-stream; name=\"" + name + "\"");
+ response.setHeader("Content-Disposition", "inline; filename=\"" + name + "\"");
+}