Bug 1310427 support protocol handlers, r=kmag
authorShane Caraveo <scaraveo@mozilla.com>
Fri, 24 Feb 2017 11:20:20 -0800
changeset 344848 704db7ae2d859e854d61cc77ae2999e843964b17
parent 344847 bafc79da8b439d7fb73a1ac016a517dc3171a131
child 344849 7ff9ada73578824a53afb4c533f8663a4c8649c1
push id31419
push userphilringnalda@gmail.com
push dateSat, 25 Feb 2017 18:25:34 +0000
treeherdermozilla-central@108bd6a1df4e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1310427
milestone54.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1310427 support protocol handlers, r=kmag MozReview-Commit-ID: 7sHh8YZWe3f
toolkit/components/extensions/ext-protocolHandlers.js
toolkit/components/extensions/extensions-toolkit.manifest
toolkit/components/extensions/jar.mn
toolkit/components/extensions/schemas/extension_protocol_handlers.json
toolkit/components/extensions/schemas/jar.mn
toolkit/components/extensions/schemas/manifest.json
toolkit/components/extensions/test/mochitest/chrome.ini
toolkit/components/extensions/test/mochitest/head.js
toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html
toolkit/components/extensions/test/mochitest/test_ext_webrequest_auth.html
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;