Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Errors

<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test for protocol handlers</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
<script type="text/javascript" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<script type="text/javascript">
"use strict";
/* eslint-disable mozilla/balanced-listeners */
function protocolChromeScript() {
const PROTOCOL_HANDLER_OPEN_PERM_KEY = "open-protocol-handler";
const PERMISSION_KEY_DELIMITER = "^";
/* eslint-env mozilla/chrome-script */
addMessageListener("setup", ({ protocol, principalOrigins }) => {
let data = {};
const protoSvc = Cc[
"@mozilla.org/uriloader/external-protocol-service;1"
].getService(Ci.nsIExternalProtocolService);
let protoInfo = protoSvc.getProtocolHandlerInfo(protocol);
data.preferredAction = protoInfo.preferredAction == protoInfo.useHelperApp;
let handlers = protoInfo.possibleApplicationHandlers;
data.handlers = handlers.length;
let handler = handlers.queryElementAt(0, Ci.nsIHandlerApp);
data.isWebHandler = handler instanceof Ci.nsIWebHandlerApp;
data.uriTemplate = handler.uriTemplate;
// ext+ protocols should be set as default when there is only one
data.preferredApplicationHandler =
protoInfo.preferredApplicationHandler == handler;
data.alwaysAskBeforeHandling = protoInfo.alwaysAskBeforeHandling;
const handlerSvc = Cc[
"@mozilla.org/uriloader/handler-service;1"
].getService(Ci.nsIHandlerService);
handlerSvc.store(protoInfo);
for (let origin of principalOrigins) {
let principal = Services.scriptSecurityManager.createContentPrincipal(
Services.io.newURI(origin),
{}
);
let pbPrincipal = Services.scriptSecurityManager.createContentPrincipal(
Services.io.newURI(origin),
{
privateBrowsingId: 1,
}
);
let permKey =
PROTOCOL_HANDLER_OPEN_PERM_KEY + PERMISSION_KEY_DELIMITER + protocol;
Services.perms.addFromPrincipal(
principal,
permKey,
Services.perms.ALLOW_ACTION,
Services.perms.EXPIRE_NEVER
);
Services.perms.addFromPrincipal(
pbPrincipal,
permKey,
Services.perms.ALLOW_ACTION,
Services.perms.EXPIRE_NEVER
);
}
sendAsyncMessage("handlerData", data);
});
addMessageListener("setPreferredAction", data => {
let { protocol, template } = data;
const protoSvc = Cc[
"@mozilla.org/uriloader/external-protocol-service;1"
].getService(Ci.nsIExternalProtocolService);
let protoInfo = protoSvc.getProtocolHandlerInfo(protocol);
for (let handler of protoInfo.possibleApplicationHandlers.enumerate()) {
if (handler.uriTemplate.startsWith(template)) {
protoInfo.preferredApplicationHandler = handler;
protoInfo.preferredAction = protoInfo.useHelperApp;
protoInfo.alwaysAskBeforeHandling = false;
}
}
const handlerSvc = Cc[
"@mozilla.org/uriloader/handler-service;1"
].getService(Ci.nsIHandlerService);
handlerSvc.store(protoInfo);
sendAsyncMessage("set");
});
}
add_task(async function test_protocolHandler() {
let extensionData = {
manifest: {
protocol_handlers: [
{
protocol: "ext+foo",
name: "a foo protocol handler",
uriTemplate: "foo.html?val=%s",
},
],
},
background() {
browser.test.onMessage.addListener(async (msg, arg) => {
if (msg == "open") {
let tab = await browser.tabs.create({ url: arg });
browser.test.sendMessage("opened", tab.id);
} else if (msg == "close") {
await browser.tabs.remove(arg);
browser.test.sendMessage("closed");
}
});
browser.test.sendMessage("test-url", browser.runtime.getURL("foo.html"));
},
files: {
"foo.js": function() {
browser.test.sendMessage("test-query", location.search);
browser.tabs.getCurrent().then(tab => browser.test.sendMessage("test-tab", tab.id));
},
"foo.html": `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="foo.js"><\/script>
</head>
</html>`,
},
};
let pb_extension = ExtensionTestUtils.loadExtension({
background() {
browser.test.onMessage.addListener(async (msg, arg) => {
if (msg == "open") {
let win = await browser.windows.create({ url: arg, incognito: true });
browser.test.sendMessage("opened", {
windowId: win.id,
tabId: win.tabs[0].id,
});
} else if (msg == "nav") {
await browser.tabs.update(arg.tabId, { url: arg.url });
browser.test.sendMessage("navigated");
} else if (msg == "close") {
await browser.windows.remove(arg);
browser.test.sendMessage("closed");
}
});
},
incognitoOverride: "spanning",
});
await pb_extension.startup();
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
let handlerUrl = await extension.awaitMessage("test-url");
// Ensure that the protocol handler is configured, and set it as default to
// bypass the dialog.
let chromeScript = SpecialPowers.loadChromeScript(protocolChromeScript);
let msg = chromeScript.promiseOneMessage("handlerData");
chromeScript.sendAsyncMessage("setup", {
protocol: "ext+foo",
principalOrigins: [
`moz-extension://${extension.uuid}/`,
`moz-extension://${pb_extension.uuid}/`,
],
});
let data = await msg;
ok(
data.preferredAction,
"using a helper application is the preferred action"
);
ok(data.preferredApplicationHandler, "handler was set as default handler");
is(data.handlers, 1, "one handler is set");
ok(!data.alwaysAskBeforeHandling, "will not show dialog");
ok(data.isWebHandler, "the handler is a web handler");
is(data.uriTemplate, `${handlerUrl}?val=%s`, "correct url template");
chromeScript.destroy();
extension.sendMessage("open", "ext+foo:test");
let id = await extension.awaitMessage("opened");
let query = await extension.awaitMessage("test-query");
is(query, "?val=ext%2Bfoo%3Atest", "test query ok");
is(id, await extension.awaitMessage("test-tab"), "id should match opened tab");
extension.sendMessage("close", id);
await extension.awaitMessage("closed");
// Test that handling a URL from the commandline works.
chromeScript = SpecialPowers.loadChromeScript(() => {
/* eslint-env mozilla/chrome-script */
const CONTENT_HANDLING_URL =
"chrome://mozapps/content/handling/permissionDialog.xhtml";
const { BrowserTestUtils } = ChromeUtils.importESModule(
);
let cmdLineHandler = Cc["@mozilla.org/browser/final-clh;1"].getService(
Ci.nsICommandLineHandler
);
let fakeCmdLine = Cu.createCommandLine(
["-url", "ext+foo:cmdline"],
null,
Ci.nsICommandLine.STATE_REMOTE_EXPLICIT
);
cmdLineHandler.handle(fakeCmdLine);
// We aren't awaiting for this promise to resolve since it returns undefined
// (because it returns the reference to the dialog window that we close, below)
// once the callback promise below finishes, and because its not needed for anything
// outside of this loadChromeScript block.
BrowserTestUtils.promiseAlertDialogOpen(
null,
CONTENT_HANDLING_URL,
{
isSubDialog: true,
async callback(dialogWin) {
is(dialogWin.document.documentURI, CONTENT_HANDLING_URL, "Open dialog is the permission dialog")
let closePromise = BrowserTestUtils.waitForEvent(
dialogWin.browsingContext.topChromeWindow,
"dialogclose",
true,
);
let dialog = dialogWin.document.querySelector("dialog");
let btn = dialog.getButton("accept");
// The security delay disables this button, just bypass it.
btn.disabled = false;
btn.click();
return closePromise;
}
}
);
});
query = await extension.awaitMessage("test-query");
is(query, "?val=ext%2Bfoo%3Acmdline", "cmdline query ok");
id = await extension.awaitMessage("test-tab");
extension.sendMessage("close", id);
await extension.awaitMessage("closed");
chromeScript.destroy();
// Test the protocol in a private window, watch for the
// console error.
consoleMonitor.start([{ message: /NS_ERROR_FILE_NOT_FOUND/ }]);
// Expect the chooser window to be open, close it.
chromeScript = SpecialPowers.loadChromeScript(() => {
/* eslint-env mozilla/chrome-script */
const CONTENT_HANDLING_URL =
"chrome://mozapps/content/handling/appChooser.xhtml";
const { BrowserTestUtils } = ChromeUtils.importESModule(
);
sendAsyncMessage("listenWindow");
// We aren't awaiting for this promise to resolve since it returns undefined
// (because it returns the reference to the dialog window that we close, below)
// once the callback promise below finishes, and because its not needed for anything
// outside of this loadChromeScript block.
BrowserTestUtils.promiseAlertDialogOpen(
null,
CONTENT_HANDLING_URL,
{
isSubDialog: true,
async callback(dialogWin) {
is(dialogWin.document.documentURI, CONTENT_HANDLING_URL, "Open dialog is the app chooser dialog")
let entry = dialogWin.document.getElementById("items")
.firstChild;
sendAsyncMessage("handling", {
name: entry.getAttribute("name"),
disabled: entry.disabled,
});
let closePromise = BrowserTestUtils.waitForEvent(
dialogWin.browsingContext.topChromeWindow,
"dialogclose",
true,
);
dialogWin.close();
return closePromise;
}
}
);
sendAsyncMessage("listenDialog");
});
// Wait for the chrome script to attach window listener
await chromeScript.promiseOneMessage("listenWindow");
let listenDialog = chromeScript.promiseOneMessage("listenDialog");
let windowOpen = pb_extension.awaitMessage("opened");
pb_extension.sendMessage("open", "ext+foo:test");
// Wait for chrome script to attach dialog listener
await listenDialog;
let { tabId, windowId } = await windowOpen;
let testData = chromeScript.promiseOneMessage("handling");
let navPromise = pb_extension.awaitMessage("navigated");
pb_extension.sendMessage("nav", { url: "ext+foo:test", tabId });
await navPromise;
await consoleMonitor.finished();
let entry = await testData;
is(entry.name, "a foo protocol handler", "entry is correct");
ok(entry.disabled, "handler is disabled");
let promiseClosed = pb_extension.awaitMessage("closed");
pb_extension.sendMessage("close", windowId);
await promiseClosed;
await pb_extension.unload();
// Shutdown the addon, then ensure the protocol was removed.
await extension.unload();
chromeScript = SpecialPowers.loadChromeScript(() => {
/* eslint-env mozilla/chrome-script */
addMessageListener("setup", () => {
const protoSvc = Cc[
"@mozilla.org/uriloader/external-protocol-service;1"
].getService(Ci.nsIExternalProtocolService);
let protoInfo = protoSvc.getProtocolHandlerInfo("ext+foo");
sendAsyncMessage(
"preferredApplicationHandler",
!protoInfo.preferredApplicationHandler
);
let handlers = protoInfo.possibleApplicationHandlers;
sendAsyncMessage("handlerData", {
preferredApplicationHandler: !protoInfo.preferredApplicationHandler,
handlers: handlers.length,
});
});
});
msg = chromeScript.promiseOneMessage("handlerData");
chromeScript.sendAsyncMessage("setup");
data = await msg;
ok(data.preferredApplicationHandler, "no preferred handler is set");
is(data.handlers, 0, "no handler is set");
chromeScript.destroy();
});
add_task(async function test_protocolHandler_two() {
let extensionData = {
manifest: {
protocol_handlers: [
{
protocol: "ext+foo",
name: "a foo protocol handler",
uriTemplate: "foo.html?val=%s",
},
{
protocol: "ext+foo",
name: "another foo protocol handler",
uriTemplate: "foo2.html?val=%s",
},
],
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
// Ensure that the protocol handler is configured, and set it as default,
// but because there are two handlers, the dialog is not bypassed. We
// don't test the actual dialog ui, it's been here forever and works based
// on the alwaysAskBeforeHandling value.
let chromeScript = SpecialPowers.loadChromeScript(protocolChromeScript);
let msg = chromeScript.promiseOneMessage("handlerData");
chromeScript.sendAsyncMessage("setup", {
protocol: "ext+foo",
principalOrigins: [],
});
let data = await msg;
ok(
data.preferredAction,
"using a helper application is the preferred action"
);
ok(data.preferredApplicationHandler, "preferred handler is set");
is(data.handlers, 2, "two handlers are set");
ok(data.alwaysAskBeforeHandling, "will show dialog");
ok(data.isWebHandler, "the handler is a web handler");
chromeScript.destroy();
await extension.unload();
});
add_task(async function test_protocolHandler_https_target() {
let extensionData = {
manifest: {
protocol_handlers: [
{
protocol: "ext+foo",
name: "http target",
},
],
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
ok(true, "https uriTemplate target works");
await extension.unload();
});
add_task(async function test_protocolHandler_http_target() {
let extensionData = {
manifest: {
protocol_handlers: [
{
protocol: "ext+foo",
name: "http target",
},
],
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
ok(true, "http uriTemplate target works");
await extension.unload();
});
add_task(async function test_protocolHandler_restricted_protocol() {
let extensionData = {
manifest: {
protocol_handlers: [
{
protocol: "http",
name: "take over the http protocol",
uriTemplate: "http.html?val=%s",
},
],
},
};
consoleMonitor.start([
{ message: /processing protocol_handlers\.0\.protocol/ },
]);
let extension = ExtensionTestUtils.loadExtension(extensionData);
await Assert.rejects(
extension.startup(),
/startup failed/,
"unable to register restricted handler protocol"
);
await consoleMonitor.finished();
});
add_task(async function test_protocolHandler_restricted_uriTemplate() {
let extensionData = {
manifest: {
protocol_handlers: [
{
protocol: "ext+foo",
name: "take over the http protocol",
uriTemplate: "ftp://example.com/file.txt",
},
],
},
};
consoleMonitor.start([
{ message: /processing protocol_handlers\.0\.uriTemplate/ },
]);
let extension = ExtensionTestUtils.loadExtension(extensionData);
await Assert.rejects(
extension.startup(),
/startup failed/,
"unable to register restricted handler uriTemplate"
);
await consoleMonitor.finished();
});
add_task(async function test_protocolHandler_duplicate() {
let extensionData = {
manifest: {
protocol_handlers: [
{
protocol: "ext+foo",
name: "foo protocol",
uriTemplate: "foo.html?val=%s",
},
{
protocol: "ext+foo",
name: "foo protocol",
uriTemplate: "foo.html?val=%s",
},
],
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
// Get the count of handlers installed.
let chromeScript = SpecialPowers.loadChromeScript(() => {
/* eslint-env mozilla/chrome-script */
addMessageListener("setup", () => {
const protoSvc = Cc[
"@mozilla.org/uriloader/external-protocol-service;1"
].getService(Ci.nsIExternalProtocolService);
let protoInfo = protoSvc.getProtocolHandlerInfo("ext+foo");
let handlers = protoInfo.possibleApplicationHandlers;
sendAsyncMessage("handlerData", handlers.length);
});
});
let msg = chromeScript.promiseOneMessage("handlerData");
chromeScript.sendAsyncMessage("setup");
let data = await msg;
is(data, 1, "cannot re-register the same handler config");
chromeScript.destroy();
await extension.unload();
});
// Test that the tabs.onUpdated event is fired for extension protocol handlers,
// even if the protocol handler does not exist and opens the error page for
// "The address wasn’t understood".
add_task(async function test_protocolHandler_onUpdated_missing_handler() {
let extensionData = {
manifest: {
permissions: ["tabs"]
},
background() {
let urlSeen = false;
let tabPromise = Promise.withResolvers();
const updateListener = async (tabId, changeInfo) => {
let tab = await tabPromise.promise;
if (tab.id != tabId) {
return;
}
browser.test.log(`tabs.onUpdated fired with changeInfo ${JSON.stringify(changeInfo)}`);
if (!urlSeen && changeInfo.url == "ext+missing:test") {
urlSeen = true;
}
if (urlSeen && changeInfo.status == "complete") {
browser.tabs.onUpdated.removeListener(updateListener);
await browser.tabs.remove(tabId);
browser.test.notifyPass();
}
};
browser.tabs.onUpdated.addListener(updateListener);
browser.test.log("Waiting for tabs.onUpdated event for 'ext+missing:test' with `complete` status.");
browser.tabs.create({ url: "ext+missing:test" }).then(
tab => tabPromise.resolve(tab)
);
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
await extension.awaitFinish();
await extension.unload();
});
// Test that a protocol handler will work if ftp is enabled
add_task(async function test_ftp_protocolHandler() {
await SpecialPowers.pushPrefEnv({
set: [
// Disabling the external protocol permission prompt. We don't need it
// for this test.
["security.external_protocol_requires_permission", false],
],
});
let extensionData = {
manifest: {
protocol_handlers: [
{
protocol: "ftp",
name: "an ftp protocol handler",
uriTemplate: "ftp.html?val=%s",
},
],
},
async background() {
browser.test.onMessage.addListener(async () => {
await browser.tabs.create({ url });
});
},
files: {
"ftp.js": function() {
browser.test.sendMessage("test-query", location.search);
},
"ftp.html": `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="ftp.js"><\/script>
</head>
</html>`,
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
const handlerUrl = `moz-extension://${extension.uuid}/ftp.html`;
let chromeScript = SpecialPowers.loadChromeScript(protocolChromeScript);
// Set the preferredAction to this extension as ftp will default to system. If
// we didn't bypass the dialog for this test, the user would get asked in this case.
let msg = chromeScript.promiseOneMessage("set");
chromeScript.sendAsyncMessage("setPreferredAction", {
protocol: "ftp",
template: handlerUrl,
});
await msg;
msg = chromeScript.promiseOneMessage("handlerData");
chromeScript.sendAsyncMessage("setup", { protocol: "ftp", principalOrigins: [] });
let data = await msg;
ok(
data.preferredAction,
"using a helper application is the preferred action"
);
ok(data.preferredApplicationHandler, "handler was set as default handler");
is(data.handlers, 1, "one handler is set");
ok(!data.alwaysAskBeforeHandling, "will not show dialog");
ok(data.isWebHandler, "the handler is a web handler");
is(data.uriTemplate, `${handlerUrl}?val=%s`, "correct url template");
chromeScript.destroy();
extension.sendMessage("run");
let query = await extension.awaitMessage("test-query");
is(query, "?val=ftp%3A%2F%2Fexample.com%2Ffile.txt", "test query ok");
await extension.unload();
});
</script>
</body>
</html>