/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; requestLongerTimeout(2); const { Downloads } = ChromeUtils.importESModule( "resource://gre/modules/Downloads.sys.mjs" ); const { DownloadIntegration } = ChromeUtils.importESModule( "resource://gre/modules/DownloadIntegration.sys.mjs" ); const TEST_PATH = getRootDirectory(gTestPath).replace( "chrome://mochitests/content", "https://example.com" ); const MimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); const HandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( Ci.nsIHandlerService ); let MockFilePicker = SpecialPowers.MockFilePicker; MockFilePicker.init(window.browsingContext); 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) { // Runs tests after all "load" event handlers have fired off let loadPromise = BrowserTestUtils.waitForContentEvent( browser, "documentloaded", false, null, true ); BrowserTestUtils.startLoadingURIString(browser, url); return loadPromise; } /** * This test covers which choices are presented for downloaded files and how * those choices are handled. Unless a pref is enabled * (browser.download.always_ask_before_handling_new_types) the unknown content * dialog will be skipped altogether by default when downloading. * To retain coverage for the non-default scenario, each task sets `alwaysAskBeforeHandling` * to true for the relevant mime-type and extensions. */ function alwaysAskForHandlingTypes(typeExtensions, ask = true) { let mimeInfos = []; for (let [type, ext] of Object.entries(typeExtensions)) { const mimeInfo = MimeSvc.getFromTypeAndExtension(type, ext); mimeInfo.alwaysAskBeforeHandling = ask; if (!ask) { mimeInfo.preferredAction = mimeInfo.handleInternally; } HandlerSvc.store(mimeInfo); mimeInfos.push(mimeInfo); } return mimeInfos; } add_setup(async function () { // 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 = 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"); registerRestoreHandler("image/webp", "webp"); }); /** * 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() { const mimeInfosToRestore = alwaysAskForHandlingTypes({ "application/pdf": "pdf", "binary/octet-stream": "pdf", }); 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, opening: TEST_PATH + file, waitForLoad: false, waitForStateStop: true, }); // 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.startLoadingURIString( 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(); let filepickerPromise = new Promise(resolve => { MockFilePicker.showCallback = function (fp) { setTimeout(() => { resolve(fp.defaultString); }, 0); return Ci.nsIFilePicker.returnCancel; }; }); 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" ); let filename = await filepickerPromise; is(filename, file, "filename was set in filepicker"); // 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 */ } } BrowserTestUtils.removeTab(loadingTab); BrowserTestUtils.removeTab(newTab); BrowserTestUtils.removeTab(extraTab); await publicList.removeFinished(); } for (let mimeInfo of mimeInfosToRestore) { HandlerSvc.remove(mimeInfo); } }); /** * 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() { const mimeInfosToRestore = alwaysAskForHandlingTypes({ "application/pdf": "pdf", "binary/octet-stream": "pdf", }); 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, opening: TEST_PATH + file, waitForLoad: false, waitForStateStop: true, }); 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(); } for (let mimeInfo of mimeInfosToRestore) { HandlerSvc.remove(mimeInfo); } }); /** * 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 mimeInfo = MimeSvc.getFromTypeAndExtension("application/pdf", "pdf"); console.log( "mimeInfo.preferredAction is currently:", mimeInfo.preferredAction ); 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, opening: TEST_PATH + file, waitForLoad: false, waitForStateStop: true, }); 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, opening: TEST_PATH + file, waitForLoad: false, waitForStateStop: true, }); 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() { const mimeInfosToRestore = alwaysAskForHandlingTypes({ "binary/octet-stream": "xml", "image/webp": "webp", }); 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_green.webp", true], ]) { let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); let loadingTab = await BrowserTestUtils.openNewForegroundTab({ gBrowser, opening: TEST_PATH + file, waitForLoad: false, waitForStateStop: true, }); 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); let fileDesc = file.substring(file.lastIndexOf(".") + 1); ok( !internalHandlerRadio.hidden, `The option should be visible for ${fileDesc}` ); if (checkDefault) { ok( internalHandlerRadio.selected, `The option should be selected for ${fileDesc}` ); } let dialog = doc.querySelector("#unknownContentType"); dialog.cancelDialog(); BrowserTestUtils.removeTab(loadingTab); } for (let mimeInfo of mimeInfosToRestore) { HandlerSvc.remove(mimeInfo); } } ); /** * 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() { const mimeInfosToRestore = alwaysAskForHandlingTypes({ "text/plain": "txt", }); let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); let loadingTab = await BrowserTestUtils.openNewForegroundTab({ gBrowser, opening: TEST_PATH + "file_txt_attachment_test.txt", waitForLoad: false, waitForStateStop: true, }); 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); for (let mimeInfo of mimeInfosToRestore) { HandlerSvc.remove(mimeInfo); } }); /** * 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() { const mimeInfosToRestore = alwaysAskForHandlingTypes({ "application/pdf": "pdf", "binary/octet-stream": "pdf", }); 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, opening: TEST_PATH + file, waitForLoad: false, waitForStateStop: true, }); 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); } for (let mimeInfo of mimeInfosToRestore) { HandlerSvc.remove(mimeInfo); } }); /** * 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() { const mimeInfosToRestore = alwaysAskForHandlingTypes({ "text/xml": "xml", }); await SpecialPowers.pushPrefEnv({ set: [["browser.helperApps.showOpenOptionForViewableInternally", false]], }); let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); let loadingTab = await BrowserTestUtils.openNewForegroundTab({ gBrowser, opening: TEST_PATH + "file_xml_attachment_test.xml", waitForLoad: false, waitForStateStop: true, }); 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); for (let mimeInfo of mimeInfosToRestore) { HandlerSvc.remove(mimeInfo); } } ); /* * This test sets the action to internal. The files should open directly without asking. */ add_task(async function test_check_open_with_internal_handler_noask() { const mimeInfosToRestore = alwaysAskForHandlingTypes( { "application/pdf": "pdf", "binary/octet-stream": "pdf", "application/octet-stream": "pdf", }, false ); // Build the matrix of tests to perform. let matrix = { alwaysOpenPDFInline: [false, true], file: [ "file_pdf_application_pdf.pdf", "file_pdf_binary_octet_stream.pdf", "file_pdf_application_octet_stream.pdf", ], where: ["top", "popup", "frame"], }; let tests = [{}]; for (let [key, values] of Object.entries(matrix)) { tests = tests.flatMap(test => values.map(value => ({ [key]: value, ...test })) ); } for (let test of tests) { info(`test case: ${JSON.stringify(test)}`); let { alwaysOpenPDFInline, file, where } = test; // These are the cases that can be opened inline. binary/octet-stream // isn't handled by pdfjs. let canHandleInline = file == "file_pdf_application_pdf.pdf" || (file == "file_pdf_application_octet_stream.pdf" && where != "frame"); await SpecialPowers.pushPrefEnv({ set: [ ["browser.helperApps.showOpenOptionForPdfJS", true], ["browser.helperApps.showOpenOptionForViewableInternally", true], ["browser.download.open_pdf_attachments_inline", alwaysOpenPDFInline], ], }); async function doNavigate(browser) { await SpecialPowers.spawn( browser, [TEST_PATH + file, where], async (contentUrl, where_) => { switch (where_) { case "top": content.location = contentUrl; break; case "popup": content.open(contentUrl); break; case "frame": let frame = content.document.createElement("iframe"); frame.setAttribute("src", contentUrl); content.document.body.appendChild(frame); break; default: ok(false, "Unknown where value"); break; } } ); } // If this is true, the pdf is opened directly without downloading it. // Otherwise, it must first be downloaded and optionally displayed in // a tab with a file url. let openPDFDirectly = alwaysOpenPDFInline && canHandleInline; await BrowserTestUtils.withNewTab( { gBrowser, url: TEST_PATH + "blank.html" }, async browser => { let readyPromise = BrowserTestUtils.waitForNewTab( gBrowser, null, false, !openPDFDirectly ); await doNavigate(browser); await readyPromise; is( gBrowser.selectedBrowser.currentURI.scheme, openPDFDirectly ? "https" : "file", "Loaded PDF uri has the correct scheme" ); // intentionally don't bother checking session history without ship to // keep complexity down. if (Services.appinfo.sessionHistoryInParent) { let shistory = browser.browsingContext.sessionHistory; is(shistory.count, 1, "should a single shentry"); is(shistory.index, 0, "should be on the first entry"); let shentry = shistory.getEntryAtIndex(shistory.index); is(shentry.URI.spec, TEST_PATH + "blank.html"); } await SpecialPowers.spawn( browser, [TEST_PATH + "blank.html"], async blankUrl => { ok( !docShell.isAttemptingToNavigate, "should not still be attempting to navigate" ); is( content.location.href, blankUrl, "original browser hasn't navigated" ); } ); await BrowserTestUtils.removeTab(gBrowser.selectedTab); } ); } for (let mimeInfo of mimeInfosToRestore) { HandlerSvc.remove(mimeInfo); } }); add_task(async () => { MockFilePicker.cleanup(); });