Bug 1626365 better extension support for ftp removal r=robwu
authorShane Caraveo <scaraveo@mozilla.com>
Wed, 17 Mar 2021 18:37:05 +0000
changeset 571653 bcdf1f609632b0a6078cd6f98388fd826cbb1c15
parent 571652 6448c2ca2b2a5a9ba1248324152363d9c688410f
child 571654 4479568880185e1479fcdea045d3252dd8474365
push id38294
push userbtara@mozilla.com
push dateWed, 17 Mar 2021 21:25:27 +0000
treeherdermozilla-central@4d4bc56f77a1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrobwu
bugs1626365
milestone88.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 1626365 better extension support for ftp removal r=robwu Remove the ability for extensions to toggle ftp support on, and add protocol handler support. Differential Revision: https://phabricator.services.mozilla.com/D108687
toolkit/components/extensions/parent/ext-browserSettings.js
toolkit/components/extensions/schemas/browser_settings.json
toolkit/components/extensions/schemas/extension_protocol_handlers.json
toolkit/components/extensions/schemas/proxy.json
toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html
toolkit/components/extensions/test/xpcshell/test_ext_browserSettings.js
--- a/toolkit/components/extensions/parent/ext-browserSettings.js
+++ b/toolkit/components/extensions/parent/ext-browserSettings.js
@@ -249,16 +249,17 @@ this.browserSettings = class extends Ext
                 details.value
               );
             },
           }
         ),
         ftpProtocolEnabled: getSettingsAPI({
           context,
           name: "ftpProtocolEnabled",
+          readOnly: true,
           callback() {
             return Services.prefs.getBoolPref("network.ftp.enabled");
           },
         }),
         homepageOverride: getSettingsAPI({
           context,
           name: HOMEPAGE_OVERRIDE_SETTING,
           callback() {
--- a/toolkit/components/extensions/schemas/browser_settings.json
+++ b/toolkit/components/extensions/schemas/browser_settings.json
@@ -51,17 +51,17 @@
         "description": "This boolean setting controls whether the selected tab can be closed with a double click."
       },
       "contextMenuShowEvent": {
         "$ref": "types.Setting",
         "description": "Controls after which mouse event context menus popup. This setting's value is of type ContextMenuMouseEvent, which has possible values of <code>mouseup</code> and <code>mousedown</code>."
       },
       "ftpProtocolEnabled": {
         "$ref": "types.Setting",
-        "description": "This boolean setting controls whether the FTP protocol is enabled."
+        "description": "Returns whether the FTP protocol is enabled. Read-only."
       },
       "homepageOverride": {
         "$ref": "types.Setting",
         "description": "Returns the value of the overridden home page. Read-only."
       },
       "imageAnimationBehavior": {
         "$ref": "types.Setting",
         "description": "Controls the behaviour of image animation in the browser. This setting's value is of type ImageAnimationBehavior, defaulting to <code>normal</code>."
--- a/toolkit/components/extensions/schemas/extension_protocol_handlers.json
+++ b/toolkit/components/extensions/schemas/extension_protocol_handlers.json
@@ -11,17 +11,17 @@
             "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", "dat", "dweb", "geo", "gopher", "im", "ipfs", "ipns", "irc", "ircs", "magnet",
+                "bitcoin", "dat", "dweb", "ftp", "geo", "gopher", "im", "ipfs", "ipns", "irc", "ircs", "magnet",
                 "mailto", "mms", "news", "nntp", "sip", "sms", "smsto", "ssb", "ssh",
                 "tel", "urn", "webcal", "wtai", "xmpp"
               ]
             }, {
               "type": "string",
               "pattern": "^(ext|web)\\+[a-z0-9.+-]+$"
             }]
           },
--- a/toolkit/components/extensions/schemas/proxy.json
+++ b/toolkit/components/extensions/schemas/proxy.json
@@ -43,17 +43,18 @@
           "httpProxyAll":{
             "type": "boolean",
             "optional": true,
             "description": "Use the http proxy server for all protocols."
           },
           "ftp": {
             "type": "string",
             "optional": true,
-            "description": "The address of the ftp proxy, can include a port."
+            "deprecated": true,
+            "description": "The address of the ftp proxy, can include a port.  Deprecated since Firefox 88."
           },
           "ssl": {
             "type": "string",
             "optional": true,
             "description": "The address of the ssl proxy, can include a port."
           },
           "socks": {
             "type": "string",
--- a/toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html
@@ -12,63 +12,88 @@
 
 <script type="text/javascript">
 "use strict";
 
 /* eslint-disable mozilla/balanced-listeners */
 /* global addMessageListener, sendAsyncMessage */
 
 function protocolChromeScript() {
-  addMessageListener("setup", () => {
+  addMessageListener("setup", protocol => {
     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;
+    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;
+    data.uriTemplate = handler.uriTemplate;
 
     // ext+ protocols should be set as default when there is only one
-    data.preferredApplicationHandler = protoInfo.preferredApplicationHandler == handler;
+    data.preferredApplicationHandler =
+      protoInfo.preferredApplicationHandler == handler;
     data.alwaysAskBeforeHandling = protoInfo.alwaysAskBeforeHandling;
-    const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"]
-                          .getService(Ci.nsIHandlerService);
+    const handlerSvc = Cc[
+      "@mozilla.org/uriloader/handler-service;1"
+    ].getService(Ci.nsIHandlerService);
     handlerSvc.store(protoInfo);
 
     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() {
-  await SpecialPowers.pushPrefEnv({set: [
-    ["extensions.allowPrivateBrowsingByDefault", false],
-    // Disabling the external protocol permission prompt. We don't need it
-    // for this test.
-    ["security.external_protocol_requires_permission", false],
-  ]});
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      ["extensions.allowPrivateBrowsingByDefault", false],
+      // 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_handlers: [
         {
-          "protocol": "ext+foo",
-          "name": "a foo protocol handler",
-          "uriTemplate": "foo.html?val=%s",
+          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});
+          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"));
     },
@@ -87,19 +112,22 @@ add_task(async function test_protocolHan
     },
   };
 
   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("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",
@@ -110,19 +138,22 @@ add_task(async function test_protocolHan
   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");
+  chromeScript.sendAsyncMessage("setup", "ext+foo");
   let data = await msg;
-  ok(data.preferredAction, "using a helper application is the preferred action");
+  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");
@@ -131,29 +162,32 @@ add_task(async function test_protocolHan
   let query = await extension.awaitMessage("test-query");
   is(query, "?val=ext%2Bfoo%3Atest", "test query ok");
 
   extension.sendMessage("close", id);
   await extension.awaitMessage("closed");
 
   // Test the protocol in a private window, watch for the
   // console error.
-  consoleMonitor.start([{message: /NS_ERROR_FILE_NOT_FOUND/}]);
+  consoleMonitor.start([{ message: /NS_ERROR_FILE_NOT_FOUND/ }]);
 
   // Expect the chooser window to be open, close it.
   chromeScript = SpecialPowers.loadChromeScript(async () => {
-    const CONTENT_HANDLING_URL = "chrome://mozapps/content/handling/appChooser.xhtml";
-    const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm");
+    const CONTENT_HANDLING_URL =
+      "chrome://mozapps/content/handling/appChooser.xhtml";
+    const { BrowserTestUtils } = ChromeUtils.import(
+      "resource://testing-common/BrowserTestUtils.jsm"
+    );
 
     let windowOpen = BrowserTestUtils.domWindowOpenedAndLoaded();
 
     sendAsyncMessage("listenWindow");
 
     let window = await windowOpen;
-    let gBrowser = window.gBrowser
+    let gBrowser = window.gBrowser;
     let tabDialogBox = gBrowser.getTabDialogBox(gBrowser.selectedBrowser);
     let dialogStack = tabDialogBox.getTabDialogManager()._dialogStack;
 
     let checkFn = dialogEvent =>
       dialogEvent.detail.dialog?._openedURL == CONTENT_HANDLING_URL;
 
     let eventPromise = BrowserTestUtils.waitForEvent(
       dialogStack,
@@ -163,57 +197,65 @@ add_task(async function test_protocolHan
     );
 
     sendAsyncMessage("listenDialog");
 
     let event = await eventPromise;
 
     let { dialog } = event.detail;
 
-    let entry =  dialog._frame.contentDocument.getElementById("items").firstChild;
-    sendAsyncMessage("handling", {name: entry.getAttribute("name"), disabled: entry.disabled});
+    let entry = dialog._frame.contentDocument.getElementById("items")
+      .firstChild;
+    sendAsyncMessage("handling", {
+      name: entry.getAttribute("name"),
+      disabled: entry.disabled,
+    });
 
     dialog.close();
   });
 
   // Wait for the chrome script to attach window listener
   await chromeScript.promiseOneMessage("listenWindow");
 
   let listenDialog = chromeScript.promiseOneMessage("listenDialog");
-  let windowOpen =  pb_extension.awaitMessage("opened");
+  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 { tabId, windowId } = await windowOpen;
 
   let testData = chromeScript.promiseOneMessage("handling");
   let navPromise = pb_extension.awaitMessage("navigated");
-  pb_extension.sendMessage("nav", {url: "ext+foo:test", tabId});
+  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(() => {
     addMessageListener("setup", () => {
-      const protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
-                         .getService(Ci.nsIExternalProtocolService);
+      const protoSvc = Cc[
+        "@mozilla.org/uriloader/external-protocol-service;1"
+      ].getService(Ci.nsIExternalProtocolService);
       let protoInfo = protoSvc.getProtocolHandlerInfo("ext+foo");
-      sendAsyncMessage("preferredApplicationHandler", !protoInfo.preferredApplicationHandler);
+      sendAsyncMessage(
+        "preferredApplicationHandler",
+        !protoInfo.preferredApplicationHandler
+      );
       let handlers = protoInfo.possibleApplicationHandlers;
 
       sendAsyncMessage("handlerData", {
         preferredApplicationHandler: !protoInfo.preferredApplicationHandler,
         handlers: handlers.length,
       });
     });
   });
@@ -224,171 +266,260 @@ add_task(async function test_protocolHan
   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_handlers: [
         {
-          "protocol": "ext+foo",
-          "name": "a foo protocol handler",
-          "uriTemplate": "foo.html?val=%s",
+          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",
+          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");
+  chromeScript.sendAsyncMessage("setup", "ext+foo");
   let data = await msg;
-  ok(data.preferredAction, "using a helper application is the preferred action");
+  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_handlers: [
         {
-          "protocol": "ext+foo",
-          "name": "http target",
-          "uriTemplate": "https://example.com/foo.html?val=%s",
+          protocol: "ext+foo",
+          name: "http target",
+          uriTemplate: "https://example.com/foo.html?val=%s",
         },
       ],
     },
   };
 
   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_handlers: [
         {
-          "protocol": "ext+foo",
-          "name": "http target",
-          "uriTemplate": "http://example.com/foo.html?val=%s",
+          protocol: "ext+foo",
+          name: "http target",
+          uriTemplate: "http://example.com/foo.html?val=%s",
         },
       ],
     },
   };
 
   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_handlers: [
         {
-          "protocol": "http",
-          "name": "take over the http protocol",
-          "uriTemplate": "http.html?val=%s",
+          protocol: "http",
+          name: "take over the http protocol",
+          uriTemplate: "http.html?val=%s",
         },
       ],
     },
   };
 
-  consoleMonitor.start([{message: /processing protocol_handlers\.0\.protocol/}]);
+  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 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_handlers: [
         {
-          "protocol": "ext+foo",
-          "name": "take over the http protocol",
-          "uriTemplate": "ftp://example.com/file.txt",
+          protocol: "ext+foo",
+          name: "take over the http protocol",
+          uriTemplate: "ftp://example.com/file.txt",
         },
       ],
     },
   };
 
-  consoleMonitor.start([{message: /processing protocol_handlers\.0\.uriTemplate/}]);
+  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 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_handlers: [
         {
-          "protocol": "ext+foo",
-          "name": "foo protocol",
-          "uriTemplate": "foo.html?val=%s",
+          protocol: "ext+foo",
+          name: "foo protocol",
+          uriTemplate: "foo.html?val=%s",
         },
         {
-          "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(() => {
     addMessageListener("setup", () => {
-      const protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
-                         .getService(Ci.nsIExternalProtocolService);
+      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 a protocol handler will work if ftp is enabled
+add_task(async function test_ftp_protocolHandler() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      ["network.ftp.enabled", false],
+      // 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() {
+      let url = "ftp://example.com/file.txt";
+      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", "ftp");
+  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>
--- a/toolkit/components/extensions/test/xpcshell/test_ext_browserSettings.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_browserSettings.js
@@ -52,16 +52,23 @@ add_task(async function test_browser_set
     "browser.zoom.full": true,
     "browser.zoom.siteSpecific": true,
   };
 
   async function background() {
     let listeners = new Set([]);
     browser.test.onMessage.addListener(async (msg, apiName, value) => {
       let apiObj = browser.browserSettings[apiName];
+      if (msg == "get") {
+        browser.test.sendMessage("settingData", await apiObj.get({}));
+        return;
+      }
+
+      // set and setNoOp
+
       // Don't add more than one listner per apiName.  We leave the
       // listener to ensure we do not get more calls than we expect.
       if (!listeners.has(apiName)) {
         apiObj.onChange.addListener(details => {
           browser.test.sendMessage("onChange", {
             details,
             setting: apiName,
           });
@@ -202,22 +209,21 @@ add_task(async function test_browser_set
     await testSetting("closeTabsByDoubleClick", true, {
       "browser.tabs.closeTabByDblclick": true,
     });
     await testSetting("closeTabsByDoubleClick", false, {
       "browser.tabs.closeTabByDblclick": false,
     });
   }
 
-  await testSetting("ftpProtocolEnabled", false, {
-    "network.ftp.enabled": false,
-  });
-  await testSetting("ftpProtocolEnabled", true, {
-    "network.ftp.enabled": true,
-  });
+  // Bug 1699222 When the pref is removed, the API needs to be updated to always
+  // return false.  At that time it should be deprecated as well.
+  extension.sendMessage("get", "ftpProtocolEnabled");
+  let data = await extension.awaitMessage("settingData");
+  equal(data.value, Services.prefs.getBoolPref("network.ftp.enabled"));
 
   await testSetting("newTabPosition", "afterCurrent", {
     "browser.tabs.insertRelatedAfterCurrent": false,
     "browser.tabs.insertAfterCurrent": true,
   });
   await testSetting("newTabPosition", "atEnd", {
     "browser.tabs.insertRelatedAfterCurrent": false,
     "browser.tabs.insertAfterCurrent": false,