new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-protocolHandlers.js
@@ -0,0 +1,69 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyServiceGetter(this, "handlerService",
+ "@mozilla.org/uriloader/handler-service;1",
+ "nsIHandlerService");
+XPCOMUtils.defineLazyServiceGetter(this, "protocolService",
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ "nsIExternalProtocolService");
+Cu.importGlobalProperties(["URL"]);
+
+const handlers = new WeakMap();
+
+function hasHandlerApp(handlerConfig) {
+ let protoInfo = protocolService.getProtocolHandlerInfo(handlerConfig.protocol);
+ let appHandlers = protoInfo.possibleApplicationHandlers;
+ for (let i = 0; i < appHandlers.length; i++) {
+ let handler = appHandlers.queryElementAt(i, Ci.nsISupports);
+ if (handler instanceof Ci.nsIWebHandlerApp &&
+ handler.uriTemplate === handlerConfig.uriTemplate) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("manifest_protocol_handlers", (type, directive, extension, manifest) => {
+ for (let handlerConfig of manifest.protocol_handlers) {
+ if (hasHandlerApp(handlerConfig)) {
+ continue;
+ }
+
+ let handler = Cc["@mozilla.org/uriloader/web-handler-app;1"]
+ .createInstance(Ci.nsIWebHandlerApp);
+ handler.name = handlerConfig.name;
+ handler.uriTemplate = handlerConfig.uriTemplate;
+
+ let protoInfo = protocolService.getProtocolHandlerInfo(handlerConfig.protocol);
+ protoInfo.possibleApplicationHandlers.appendElement(handler, false);
+ handlerService.store(protoInfo);
+ }
+ handlers.set(extension, manifest.protocol_handlers);
+});
+
+extensions.on("shutdown", (type, extension) => {
+ if (!handlers.has(extension) || extension.shutdownReason === "APP_SHUTDOWN") {
+ return;
+ }
+ for (let handlerConfig of handlers.get(extension)) {
+ let protoInfo = protocolService.getProtocolHandlerInfo(handlerConfig.protocol);
+ let appHandlers = protoInfo.possibleApplicationHandlers;
+ for (let i = 0; i < appHandlers.length; i++) {
+ let handler = appHandlers.queryElementAt(i, Ci.nsISupports);
+ if (handler instanceof Ci.nsIWebHandlerApp &&
+ handler.uriTemplate === handlerConfig.uriTemplate) {
+ appHandlers.removeElementAt(i);
+ if (protoInfo.preferredApplicationHandler === handler) {
+ protoInfo.preferredApplicationHandler = null;
+ protoInfo.alwaysAskBeforeHandling = true;
+ }
+ handlerService.store(protoInfo);
+ break;
+ }
+ }
+ }
+ handlers.delete(extension);
+});
--- a/toolkit/components/extensions/extensions-toolkit.manifest
+++ b/toolkit/components/extensions/extensions-toolkit.manifest
@@ -6,16 +6,17 @@ category webextension-scripts cookies ch
category webextension-scripts downloads chrome://extensions/content/ext-downloads.js
category webextension-scripts geolocation chrome://extensions/content/ext-geolocation.js
category webextension-scripts management chrome://extensions/content/ext-management.js
category webextension-scripts notifications chrome://extensions/content/ext-notifications.js
category webextension-scripts i18n chrome://extensions/content/ext-i18n.js
category webextension-scripts idle chrome://extensions/content/ext-idle.js
category webextension-scripts webRequest chrome://extensions/content/ext-webRequest.js
category webextension-scripts webNavigation chrome://extensions/content/ext-webNavigation.js
+category webextension-scripts handlers chrome://extensions/content/ext-protocolHandlers.js
category webextension-scripts runtime chrome://extensions/content/ext-runtime.js
category webextension-scripts extension chrome://extensions/content/ext-extension.js
category webextension-scripts storage chrome://extensions/content/ext-storage.js
category webextension-scripts topSites chrome://extensions/content/ext-topSites.js
category webextension-scripts privacy chrome://extensions/content/ext-privacy.js
# scripts specific for content process.
category webextension-scripts-content extension chrome://extensions/content/ext-c-extension.js
@@ -45,16 +46,17 @@ category webextension-scripts-addon stor
# schemas
category webextension-schemas alarms chrome://extensions/content/schemas/alarms.json
category webextension-schemas contextualIdentities chrome://extensions/content/schemas/contextual_identities.json
category webextension-schemas cookies chrome://extensions/content/schemas/cookies.json
category webextension-schemas downloads chrome://extensions/content/schemas/downloads.json
category webextension-schemas events chrome://extensions/content/schemas/events.json
category webextension-schemas extension chrome://extensions/content/schemas/extension.json
category webextension-schemas extension_types chrome://extensions/content/schemas/extension_types.json
+category webextension-schemas handlers chrome://extensions/content/schemas/extension_protocol_handlers.json
category webextension-schemas i18n chrome://extensions/content/schemas/i18n.json
#ifndef ANDROID
category webextension-schemas identity chrome://extensions/content/schemas/identity.json
#endif
category webextension-schemas idle chrome://extensions/content/schemas/idle.json
category webextension-schemas management chrome://extensions/content/schemas/management.json
category webextension-schemas native_host_manifest chrome://extensions/content/schemas/native_host_manifest.json
category webextension-schemas notifications chrome://extensions/content/schemas/notifications.json
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -12,16 +12,17 @@ toolkit.jar:
content/extensions/ext-downloads.js
content/extensions/ext-geolocation.js
content/extensions/ext-management.js
content/extensions/ext-notifications.js
content/extensions/ext-i18n.js
content/extensions/ext-idle.js
content/extensions/ext-webRequest.js
content/extensions/ext-webNavigation.js
+ content/extensions/ext-protocolHandlers.js
content/extensions/ext-runtime.js
content/extensions/ext-extension.js
content/extensions/ext-storage.js
content/extensions/ext-topSites.js
content/extensions/ext-privacy.js
content/extensions/ext-c-backgroundPage.js
content/extensions/ext-c-extension.js
#ifndef ANDROID
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/extension_protocol_handlers.json
@@ -0,0 +1,51 @@
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "id": "ProtocolHandler",
+ "type": "object",
+ "description": "Represents a protocol handler definition.",
+ "properties": {
+ "name": {
+ "description": "A user-readable title string for the protocol handler. This will be displayed to the user in interface objects as needed.",
+ "type": "string"
+ },
+ "protocol": {
+ "description": "The protocol the site wishes to handle, specified as a string. For example, you can register to handle SMS text message links by registering to handle the \"sms\" scheme.",
+ "choices": [{
+ "type": "string",
+ "enum": [
+ "bitcoin", "geo", "im", "irc", "ircs", "magnet", "mailto",
+ "mms", "news", "nntp", "sip", "sms", "smsto", "ssh", "tel",
+ "urn", "webcal", "wtai", "xmpp"
+ ]
+ }, {
+ "type": "string",
+ "pattern": "^(ext|web)\\+[a-z0-9.+-]+$"
+ }]
+ },
+ "uriTemplate": {
+ "description": "The URL of the handler, as a string. This string should include \"%s\" as a placeholder which will be replaced with the escaped URL of the document to be handled. This URL might be a true URL, or it could be a phone number, email address, or so forth.",
+ "preprocess": "localize",
+ "choices": [
+ {"$ref": "ExtensionURL"},
+ {"$ref": "HttpURL"}
+ ]
+ }
+ }
+ },
+ {
+ "$extend": "WebExtensionManifest",
+ "properties": {
+ "protocol_handlers": {
+ "description": "A list of protocol handler definitions.",
+ "optional": true,
+ "type": "array",
+ "items": {"$ref": "ProtocolHandler"}
+ }
+ }
+ }
+ ]
+ }
+]
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -7,16 +7,17 @@ toolkit.jar:
content/extensions/schemas/alarms.json
content/extensions/schemas/contextual_identities.json
content/extensions/schemas/cookies.json
content/extensions/schemas/downloads.json
content/extensions/schemas/events.json
content/extensions/schemas/experiments.json
content/extensions/schemas/extension.json
content/extensions/schemas/extension_types.json
+ content/extensions/schemas/extension_protocol_handlers.json
content/extensions/schemas/i18n.json
#ifndef ANDROID
content/extensions/schemas/identity.json
#endif
content/extensions/schemas/idle.json
content/extensions/schemas/management.json
content/extensions/schemas/manifest.json
content/extensions/schemas/native_host_manifest.json
--- a/toolkit/components/extensions/schemas/manifest.json
+++ b/toolkit/components/extensions/schemas/manifest.json
@@ -219,16 +219,22 @@
"notifications",
"storage"
]
},
{ "$ref": "MatchPattern" }
]
},
{
+ "id": "HttpURL",
+ "type": "string",
+ "format": "url",
+ "pattern": "^https?://.*$"
+ },
+ {
"id": "ExtensionURL",
"type": "string",
"format": "strictRelativeUrl"
},
{
"id": "ExtensionID",
"choices": [
{
--- a/toolkit/components/extensions/test/mochitest/chrome.ini
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -25,16 +25,17 @@ skip-if = (toolkit == 'android') # andro
[test_chrome_native_messaging_paths.html]
skip-if = os != "mac" && os != "linux"
[test_ext_cookies_expiry.html]
[test_ext_cookies_permissions_bad.html]
[test_ext_cookies_permissions_good.html]
[test_ext_cookies_containers.html]
[test_ext_jsversion.html]
[test_ext_schema.html]
+[test_ext_protocolHandlers.html]
[test_chrome_ext_storage_cleanup.html]
[test_chrome_ext_idle.html]
[test_chrome_ext_identity.html]
skip-if = os == 'android' # unsupported.
[test_chrome_ext_downloads_saveAs.html]
[test_chrome_ext_webrequest_background_events.html]
[test_chrome_ext_webrequest_host_permissions.html]
[test_chrome_ext_trackingprotection.html]
--- a/toolkit/components/extensions/test/mochitest/head.js
+++ b/toolkit/components/extensions/test/mochitest/head.js
@@ -1,11 +1,11 @@
"use strict";
-/* exported AppConstants */
+/* exported AppConstants, Assert */
var {AppConstants} = SpecialPowers.Cu.import("resource://gre/modules/AppConstants.jsm", {});
// We run tests under two different configurations, from mochitest.ini and
// mochitest-remote.ini. When running from mochitest-remote.ini, the tests are
// copied to the sub-directory "test-oop-extensions", which we detect here, and
// use to select our configuration.
if (location.pathname.includes("test-oop-extensions")) {
@@ -30,16 +30,26 @@ if (location.pathname.includes("test-oop
chromeScript.destroy();
if (results.extraWindows.length || results.extraTabs.length) {
ok(false, `Test left extra windows or tabs: ${JSON.stringify(results)}\n`);
}
});
}
+let Assert = {
+ rejects(promise, msg) {
+ return promise.then(() => {
+ ok(false, msg);
+ }, () => {
+ ok(true, msg);
+ });
+ },
+};
+
/* exported waitForLoad */
function waitForLoad(win) {
return new Promise(resolve => {
win.addEventListener("load", function() {
resolve();
}, {capture: true, once: true});
});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html
@@ -0,0 +1,253 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for protocol handlers</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/* eslint-disable mozilla/balanced-listeners */
+/* global addMessageListener, sendAsyncMessage */
+
+add_task(function* test_protocolHandler() {
+ let extensionData = {
+ manifest: {
+ "protocol_handlers": [
+ {
+ "protocol": "ext+foo",
+ "name": "a foo protocol handler",
+ "uriTemplate": "foo.html?val=%s",
+ },
+ ],
+ },
+
+ background() {
+ browser.test.sendMessage("test-url", browser.runtime.getURL("foo.html"));
+ },
+
+ files: {
+ "foo.js": function() {
+ browser.test.sendMessage("test-query", location.search);
+ },
+ "foo.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="foo.js"><\/script>
+ </head>
+ </html>`,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ let handlerUrl = yield extension.awaitMessage("test-url");
+
+ // Ensure that the protocol handler is configured, and set it as default to
+ // bypass the dialog.
+ let chromeScript = SpecialPowers.loadChromeScript(() => {
+ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+ addMessageListener("setup", () => {
+ let data = {};
+ const protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService);
+ let protoInfo = protoSvc.getProtocolHandlerInfo("ext+foo");
+ data.preferredAction = protoInfo.preferredAction === protoInfo.useHelperApp;
+ data.preferredApplicationHandler = !protoInfo.preferredApplicationHandler;
+
+ 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;
+
+ protoInfo.preferredApplicationHandler = handler;
+ protoInfo.alwaysAskBeforeHandling = false;
+ const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"]
+ .getService(Ci.nsIHandlerService);
+ handlerSvc.store(protoInfo);
+
+ sendAsyncMessage("handlerData", data);
+ });
+ });
+
+ let msg = chromeScript.promiseOneMessage("handlerData");
+ chromeScript.sendAsyncMessage("setup");
+ let data = yield msg;
+ ok(data.preferredAction, "using a helper application is the preferred action");
+ ok(data.preferredApplicationHandler, "no preferred handler is set");
+ is(data.handlers, 1, "one handler is set");
+ ok(data.isWebHandler, "the handler is a web handler");
+ is(data.uriTemplate, `${handlerUrl}?val=%s`, "correct url template");
+ chromeScript.destroy();
+
+ let win = window.open("ext+foo:test");
+ let query = yield extension.awaitMessage("test-query");
+ is(query, "?val=ext%2Bfoo%3Atest", "test query ok");
+ win.close();
+
+ // Shutdown the addon, then ensure the protocol was removed.
+ yield extension.unload();
+ chromeScript = SpecialPowers.loadChromeScript(() => {
+ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+ 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 = yield msg;
+ ok(data.preferredApplicationHandler, "no preferred handler is set");
+ is(data.handlers, 0, "no handler is set");
+ chromeScript.destroy();
+});
+
+add_task(function* test_protocolHandler_https_target() {
+ let extensionData = {
+ manifest: {
+ "protocol_handlers": [
+ {
+ "protocol": "ext+foo",
+ "name": "http target",
+ "uriTemplate": "https://example.com/foo.html?val=%s",
+ },
+ ],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ ok(true, "https uriTemplate target works");
+ yield extension.unload();
+});
+
+add_task(function* test_protocolHandler_http_target() {
+ let extensionData = {
+ manifest: {
+ "protocol_handlers": [
+ {
+ "protocol": "ext+foo",
+ "name": "http target",
+ "uriTemplate": "http://example.com/foo.html?val=%s",
+ },
+ ],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ ok(true, "http uriTemplate target works");
+ yield extension.unload();
+});
+
+add_task(function* test_protocolHandler_restricted_protocol() {
+ let extensionData = {
+ manifest: {
+ "protocol_handlers": [
+ {
+ "protocol": "http",
+ "name": "take over the http protocol",
+ "uriTemplate": "http.html?val=%s",
+ },
+ ],
+ },
+ };
+
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [{message: /processing protocol_handlers\.0\.protocol/}]);
+ });
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield Assert.rejects(extension.startup(), "unable to register restricted handler protocol");
+
+ SimpleTest.endMonitorConsole();
+ yield waitForConsole;
+});
+
+add_task(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",
+ },
+ ],
+ },
+ };
+
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [{message: /processing protocol_handlers\.0\.uriTemplate/}]);
+ });
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield Assert.rejects(extension.startup(), "unable to register restricted handler uriTemplate");
+
+ SimpleTest.endMonitorConsole();
+ yield waitForConsole;
+});
+
+add_task(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);
+ yield extension.startup();
+
+ // Get the count of handlers installed.
+ let chromeScript = SpecialPowers.loadChromeScript(() => {
+ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+ 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 = yield msg;
+ is(data, 1, "cannot re-register the same handler config");
+ chromeScript.destroy();
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html
@@ -7,26 +7,16 @@
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
<script type="text/javascript" src="head_webrequest.js"></script>
<script type="text/javascript" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script>
"use strict";
-let Assert = {
- rejects(promise, msg) {
- return promise.then(() => {
- ok(false, msg);
- }, () => {
- ok(true, msg);
- });
- },
-};
-
let baseUrl = "http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/authenticate.sjs";
function testXHR(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = resolve;
xhr.onabort = reject;
xhr.onerror = reject;