diff options
Diffstat (limited to '')
-rw-r--r-- | uriloader/exthandler/tests/mochitest/head.js | 535 |
1 files changed, 535 insertions, 0 deletions
diff --git a/uriloader/exthandler/tests/mochitest/head.js b/uriloader/exthandler/tests/mochitest/head.js new file mode 100644 index 0000000000..183aeee20e --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/head.js @@ -0,0 +1,535 @@ +var { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +var { HandlerServiceTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/HandlerServiceTestUtils.sys.mjs" +); + +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() {}, + setDownloadToLaunch() {}, + 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; +} + +function createTemporarySaveDirectory() { + var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + if (!saveDir.exists()) { + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + return saveDir; +} + +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 + ); + console.error(ex); + } + let dlg = await helperAppDialogShownPromise; + + is( + dlg.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Got correct dialog" + ); + + return dlg; +} + +/** + * Wait for a subdialog event indicating a dialog either opened + * or was closed. + * + * First argument is the browser in which to listen. If a tabbrowser, + * we listen to subdialogs for any tab of that browser. + */ +async function waitForSubDialog(browser, url, state) { + let eventStr = state ? "dialogopen" : "dialogclose"; + + let eventTarget; + + // Tabbrowser? + if (browser.tabContainer) { + eventTarget = browser.tabContainer.ownerDocument.documentElement; + } else { + // Individual browser. Get its box: + let tabDialogBox = browser.ownerGlobal.gBrowser.getTabDialogBox(browser); + eventTarget = tabDialogBox.getTabDialogManager()._dialogStack; + } + + let checkFn; + + if (state) { + checkFn = dialogEvent => dialogEvent.detail.dialog?._openedURL == url; + } + + let event = await BrowserTestUtils.waitForEvent( + eventTarget, + 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, stopFromOpening) { + return new Promise(resolve => { + list.addView({ + onDownloadChanged(download) { + if (stopFromOpening) { + download.launchWhenSucceeded = false; + } + 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 = PathUtils.join( + PathUtils.tempDir, + "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 () { + Services.prefs.clearUserPref("browser.download.folderList"); + Services.prefs.clearUserPref("browser.download.dir"); + try { + await IOUtils.remove(tmpDir, { recursive: true }); + } catch (e) { + console.error(e); + } + }); + Services.prefs.setIntPref("browser.download.folderList", 2); + Services.prefs.setCharPref("browser.download.dir", tmpDir); + return tmpDir; +} + +add_setup(async function test_common_initialize() { + gDownloadDir = await setDownloadDir(); + Services.prefs.setCharPref("browser.download.loglevel", "Debug"); + registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.download.loglevel"); + }); +}); + +async function removeAllDownloads() { + let publicList = await Downloads.getList(Downloads.PUBLIC); + let downloads = await publicList.getAll(); + for (let download of downloads) { + await publicList.remove(download); + if (await IOUtils.exists(download.target.path)) { + await download.finalize(true); + } + } +} + +// Helpers for external protocol sandbox tests. +const EXT_PROTO_URI_MAILTO = "mailto:test@example.com"; + +/** + * Creates and iframe and navigate to an external protocol from the iframe. + * @param {MozBrowser} browser - Browser to spawn iframe in. + * @param {string} sandboxAttr - Sandbox attribute value for the iframe. + * @param {'trustedClick'|'untrustedClick'|'trustedLocationAPI'|'untrustedLocationAPI'|'frameSrc'|'frameSrcRedirect'} triggerMethod + * - How to trigger the navigation to the external protocol. + */ +async function navigateExternalProtoFromIframe( + browser, + sandboxAttr, + useCSPSandbox = false, + triggerMethod = "trustedClick" +) { + if ( + ![ + "trustedClick", + "untrustedClick", + "trustedLocationAPI", + "untrustedLocationAPI", + "frameSrc", + "frameSrcRedirect", + ].includes(triggerMethod) + ) { + throw new Error("Invalid trigger method " + triggerMethod); + } + + // Construct the url to use as iframe src. + let testPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ); + let frameSrc = testPath + "/protocol_custom_sandbox_helper.sjs"; + + // Load the external protocol directly via the frame src field. + if (triggerMethod == "frameSrc") { + frameSrc = EXT_PROTO_URI_MAILTO; + } else if (triggerMethod == "frameSrcRedirect") { + let url = new URL(frameSrc); + url.searchParams.set("redirectCustomProtocol", "true"); + frameSrc = url.href; + } + + // If enabled set the sandbox attributes via CSP header instead. To do + // this we need to pass the sandbox flags to the test server via query + // params. + if (useCSPSandbox) { + let url = new URL(frameSrc); + url.searchParams.set("cspSandbox", sandboxAttr); + frameSrc = url.href; + + // If we use CSP sandbox attributes we shouldn't set any via iframe attribute. + sandboxAttr = null; + } + + // Create a sandboxed iframe and navigate to the external protocol. + await SpecialPowers.spawn( + browser, + [sandboxAttr, frameSrc, EXT_PROTO_URI_MAILTO, triggerMethod], + async (sandbox, src, extProtoURI, trigger) => { + let frame = content.document.createElement("iframe"); + + if (sandbox != null) { + frame.sandbox = sandbox; + } + + frame.src = src; + + let useFrameSrc = trigger == "frameSrc" || trigger == "frameSrcRedirect"; + + // Create frame load promise. + let frameLoadPromise; + // We won't get a load event if we directly put the external protocol in + // the frame src. + if (!useFrameSrc) { + frameLoadPromise = ContentTaskUtils.waitForEvent(frame, "load", false); + } + + content.document.body.appendChild(frame); + await frameLoadPromise; + + if (!useFrameSrc) { + // Trigger the external protocol navigation in the iframe. We test + // navigation by clicking links and navigation via the history API. + await SpecialPowers.spawn( + frame, + [extProtoURI, trigger], + async (uri, trigger2) => { + let link = content.document.createElement("a"); + link.innerText = "CLICK ME"; + link.id = "extProtoLink"; + content.document.body.appendChild(link); + + if (trigger2 == "trustedClick" || trigger2 == "untrustedClick") { + link.href = uri; + } else if ( + trigger2 == "trustedLocationAPI" || + trigger2 == "untrustedLocationAPI" + ) { + link.setAttribute("onclick", `location.href = '${uri}'`); + } + + if ( + trigger2 == "untrustedClick" || + trigger2 == "untrustedLocationAPI" + ) { + link.click(); + } else if ( + trigger2 == "trustedClick" || + trigger2 == "trustedLocationAPI" + ) { + await ContentTaskUtils.waitForCondition( + () => link, + "wait for link to be present" + ); + await EventUtils.synthesizeMouseAtCenter(link, {}, content); + } + } + ); + } + } + ); +} + +/** + * Wait for the sandbox error message which is shown in the web console when an + * external protocol navigation from a sandboxed context is blocked. + * @returns {Promise} - Promise which resolves once message has been logged. + */ +function waitForExtProtocolSandboxError() { + return new Promise(resolve => { + Services.console.registerListener(function onMessage(msg) { + let { message, logLevel } = msg; + if (logLevel != Ci.nsIConsoleMessage.error) { + return; + } + if ( + !message.includes( + `Blocked navigation to custom protocol “${EXT_PROTO_URI_MAILTO}” from a sandboxed context.` + ) + ) { + return; + } + Services.console.unregisterListener(onMessage); + resolve(); + }); + }); +} + +/** + * Run the external protocol sandbox test using iframes. + * @param {Object} options + * @param {boolean} options.blocked - Whether the navigation should be blocked. + * @param {string} options.sandbox - See {@link navigateExternalProtoFromIframe}. + * @param {string} options.useCSPSandbox - See {@link navigateExternalProtoFromIframe}. + * @param {string} options.triggerMethod - See {@link navigateExternalProtoFromIframe}. + * @returns {Promise} - Promise which resolves once the test has finished. + */ +function runExtProtocolSandboxTest(options) { + let { blocked, sandbox, useCSPSandbox = false, triggerMethod } = options; + + let testPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ); + + info("runSandboxTest options: " + JSON.stringify(options)); + return BrowserTestUtils.withNewTab( + testPath + "/protocol_custom_sandbox_helper.sjs", + async browser => { + if (blocked) { + let errorPromise = waitForExtProtocolSandboxError(); + await navigateExternalProtoFromIframe( + browser, + sandbox, + useCSPSandbox, + triggerMethod + ); + await errorPromise; + + ok( + errorPromise, + "Should not show the dialog for iframe with sandbox " + sandbox + ); + } else { + let dialogWindowOpenPromise = waitForProtocolAppChooserDialog( + browser, + true + ); + await navigateExternalProtoFromIframe( + browser, + sandbox, + useCSPSandbox, + triggerMethod + ); + let dialog = await dialogWindowOpenPromise; + + ok(dialog, "Should show the dialog for sandbox " + sandbox); + + // Close dialog before closing the tab to avoid intermittent failures. + let dialogWindowClosePromise = waitForProtocolAppChooserDialog( + browser, + false + ); + + dialog.close(); + await dialogWindowClosePromise; + } + } + ); +} |