Source code
Revision control
Copy as Markdown
Other Tools
var { FileUtils } = ChromeUtils.importESModule(
"resource://gre/modules/FileUtils.sys.mjs"
);
var { HandlerServiceTestUtils } = ChromeUtils.importESModule(
);
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 = new FileUtils.File(
PathUtils.join(PathUtils.tempDir, "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,
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";
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(
);
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(
);
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;
}
}
);
}