Bug 1521596 incognito support for protocol handlers r=flod,kmag
authorShane Caraveo <scaraveo@mozilla.com>
Wed, 27 Feb 2019 01:45:34 +0000
changeset 519280 47bfb7727024ef69f4f160c61ff6abc8e38cdfb5
parent 519279 6d7343355004a57779714b90b347204e1d2c55d4
child 519281 705b9ae4d7280aac57cd326467387b77424aaab6
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflod, kmag
bugs1521596
milestone67.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 1521596 incognito support for protocol handlers r=flod,kmag Depends on D19081 Differential Revision: https://phabricator.services.mozilla.com/D19082
toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html
toolkit/locales/en-US/chrome/mozapps/handling/handling.properties
toolkit/mozapps/handling/content/dialog.js
toolkit/mozapps/handling/content/handler.xml
uriloader/exthandler/WebHandlerApp.jsm
--- a/toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_protocolHandlers.html
@@ -1,11 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
+  <meta charset="utf-8">
   <title>Test for protocol handlers</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <script type="text/javascript" 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>
@@ -38,16 +39,19 @@ function protocolChromeScript() {
                           .getService(Ci.nsIHandlerService);
     handlerSvc.store(protoInfo);
 
     sendAsyncMessage("handlerData", data);
   });
 }
 
 add_task(async function test_protocolHandler() {
+  await SpecialPowers.pushPrefEnv({set: [
+    ["extensions.allowPrivateBrowsingByDefault", false],
+  ]});
   let extensionData = {
     manifest: {
       "protocol_handlers": [
         {
           "protocol": "ext+foo",
           "name": "a foo protocol handler",
           "uriTemplate": "foo.html?val=%s",
         },
@@ -76,16 +80,32 @@ add_task(async function test_protocolHan
           <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 tab = await browser.windows.create({url: arg, incognito: true});
+          browser.test.sendMessage("opened", tab.id);
+        } 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);
 
@@ -104,16 +124,47 @@ add_task(async function test_protocolHan
   let id = await extension.awaitMessage("opened");
 
   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/}]);
+
+  // Expect the chooser window to be open, close it.
+  chromeScript = SpecialPowers.loadChromeScript(async () => {
+    const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm");
+
+    let window = await BrowserTestUtils.domWindowOpened(undefined, win => {
+      return BrowserTestUtils.waitForEvent(win, "load", false, event => {
+        let win = event.target.defaultView;
+        return win.document.documentElement.getAttribute("id") === "handling";
+      });
+    });
+    let entry =  window.document.getElementById("items").firstChild;
+    sendAsyncMessage("handling", {name: entry.getAttribute("name"), disabled: entry.disabled});
+    window.close();
+  });
+
+  let testData = chromeScript.promiseOneMessage("handling");
+  pb_extension.sendMessage("open", "ext+foo:test");
+  id = await pb_extension.awaitMessage("opened");
+  await consoleMonitor.finished();
+  let entry = await testData;
+  is(entry.name, "a foo protocol handler", "entry is correct");
+  ok(entry.disabled, "handler is disabled");
+
+  pb_extension.sendMessage("close", id);
+  await pb_extension.awaitMessage("closed");
+  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);
       let protoInfo = protoSvc.getProtocolHandlerInfo("ext+foo");
       sendAsyncMessage("preferredApplicationHandler", !protoInfo.preferredApplicationHandler);
--- a/toolkit/locales/en-US/chrome/mozapps/handling/handling.properties
+++ b/toolkit/locales/en-US/chrome/mozapps/handling/handling.properties
@@ -4,9 +4,12 @@
 
 protocol.title=Launch Application
 protocol.description=This link needs to be opened with an application.
 protocol.choices.label=Send to:
 protocol.checkbox.label=Remember my choice for %S links.
 protocol.checkbox.accesskey=R
 protocol.checkbox.extra=This can be changed in %S’s preferences.
 
+# Displayed under the name of a protocol handler in the Launch Application dialog.
+privatebrowsing.disabled.label=Disabled in Private Windows
+
 choose.application.title=Another Application…
--- a/toolkit/mozapps/handling/content/dialog.js
+++ b/toolkit/mozapps/handling/content/dialog.js
@@ -25,17 +25,17 @@
  * window.arguments[8]:
  *   This is the nsIURI that we are being brought up for in the first place.
  * window.arguments[9]:
  *   The nsIInterfaceRequestor of the parent window; may be null
  */
 
 const {EnableDelayHelper} = ChromeUtils.import("resource://gre/modules/SharedPromptUtils.jsm");
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
+const {PrivateBrowsingUtils} = ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 var dialog = {
   // Member Variables
 
   _handlerInfo: null,
   _URI: null,
   _itemChoose: null,
   _okButton: null,
@@ -46,18 +46,36 @@ var dialog = {
 
  /**
   * This function initializes the content of the dialog.
   */
   initialize: function initialize() {
     this._handlerInfo = window.arguments[7].QueryInterface(Ci.nsIHandlerInfo);
     this._URI         = window.arguments[8].QueryInterface(Ci.nsIURI);
     this._windowCtxt  = window.arguments[9];
-    if (this._windowCtxt)
+    let usePrivateBrowsing = false;
+    if (this._windowCtxt) {
+      // The context should be nsIRemoteWindowContext in OOP, or nsIDOMWindow otherwise.
+      try {
+        usePrivateBrowsing = this._windowCtxt.getInterface(Ci.nsIRemoteWindowContext)
+                                             .usePrivateBrowsing;
+      } catch (e) {
+        try {
+          let opener = this._windowCtxt.getInterface(Ci.nsIDOMWindow);
+          usePrivateBrowsing = PrivateBrowsingUtils.isContentWindowPrivate(opener);
+        } catch (e) {
+          Cu.reportError(`No interface to determine privateness: ${e}`);
+        }
+      }
       this._windowCtxt.QueryInterface(Ci.nsIInterfaceRequestor);
+    }
+
+    this.isPrivate = usePrivateBrowsing ||
+                     (window.opener && PrivateBrowsingUtils.isWindowPrivate(window.opener));
+
     this._itemChoose  = document.getElementById("item-choose");
     this._okButton    = document.documentElement.getButton("accept");
 
     var description = {
       image: document.getElementById("description-image"),
       text:  document.getElementById("description-text"),
     };
     var options = document.getElementById("item-action-text");
@@ -120,16 +138,31 @@ var dialog = {
           // because the service looks for a record with the exact URL we give
           // it, and users won't have such records for URLs they don't visit,
           // and users won't visit the handler's URL template, they'll only
           // visit URLs derived from that template (i.e. with %s in the template
           // replaced by the URL of the content being handled).
           elm.setAttribute("image", uri.prePath + "/favicon.ico");
         }
         elm.setAttribute("description", uri.prePath);
+
+        // Check for extensions needing private browsing access before
+        // creating UI elements.
+        if (this.isPrivate) {
+          let policy = WebExtensionPolicy.getByURI(uri);
+          if (policy && !policy.privateBrowsingAllowed) {
+            var bundle = document.getElementById("base-strings");
+            var disabledLabel = bundle.getString("privatebrowsing.disabled.label");
+            elm.setAttribute("disabled", true);
+            elm.setAttribute("description", disabledLabel);
+            if (app == preferredHandler) {
+              preferredHandler = null;
+            }
+          }
+        }
       } else if (app instanceof Ci.nsIDBusHandlerApp) {
         elm.setAttribute("description", app.method);
       } else if (!(app instanceof Ci.nsIGIOMimeApp)) {
         // We support GIO application handler, but no action required there
         throw "unknown handler type";
       }
 
       items.insertBefore(elm, this._itemChoose);
--- a/toolkit/mozapps/handling/content/handler.xml
+++ b/toolkit/mozapps/handling/content/handler.xml
@@ -8,21 +8,21 @@
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:xbl="http://www.mozilla.org/xbl">
 
   <binding id="handler"
            extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
 
     <content>
       <xul:vbox pack="center">
-        <xul:image xbl:inherits="src=image" height="32" width="32"/>
+        <xul:image xbl:inherits="src=image,disabled" height="32" width="32"/>
       </xul:vbox>
       <xul:vbox flex="1">
-        <xul:label class="name" xbl:inherits="value=name"/>
-        <xul:label class="description" xbl:inherits="value=description"/>
+        <xul:label class="name" xbl:inherits="value=name,disabled"/>
+        <xul:label class="description" xbl:inherits="value=description,disabled"/>
       </xul:vbox>
     </content>
     <implementation>
       <property name="label" onget="return this.getAttribute('name') + ' ' + this.getAttribute('description');"/>
     </implementation>
   </binding>
 
 </bindings>
--- a/uriloader/exthandler/WebHandlerApp.jsm
+++ b/uriloader/exthandler/WebHandlerApp.jsm
@@ -1,16 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 const {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
+ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
 function nsWebHandlerApp() {}
 
 nsWebHandlerApp.prototype = {
   classDescription: "A web handler for protocols and content",
   classID: Components.ID("8b1ae382-51a9-4972-b930-56977a57919d"),
   contractID: "@mozilla.org/uriloader/web-handler-app;1",
   QueryInterface: ChromeUtils.generateQI([Ci.nsIWebHandlerApp, Ci.nsIHandlerApp]),
 
@@ -58,32 +59,53 @@ nsWebHandlerApp.prototype = {
 
     // encode the URI to be handled
     var escapedUriSpecToHandle = encodeURIComponent(aURI.spec);
 
     // insert the encoded URI and create the object version.
     var uriSpecToSend = this.uriTemplate.replace("%s", escapedUriSpecToHandle);
     var uriToSend = Services.io.newURI(uriSpecToSend);
 
+    let policy = WebExtensionPolicy.getByURI(uriToSend);
+    let privateAllowed = !policy || policy.privateBrowsingAllowed;
+
     // if we have a window context, use the URI loader to load there
     if (aWindowContext) {
       try {
+        let remoteWindow = aWindowContext.getInterface(Ci.nsIRemoteWindowContext);
+        if (remoteWindow.usePrivateBrowsing && !privateAllowed) {
+          throw Components.Exception("Extension not allowed in private windows.",
+                                     Cr.NS_ERROR_FILE_NOT_FOUND);
+        }
         // getInterface throws if the object doesn't implement the given
         // interface, so this try/catch statement is more of an if.
         // If aWindowContext refers to a remote docshell, send the load
         // request to the correct process.
-        aWindowContext.getInterface(Ci.nsIRemoteWindowContext)
-                      .openURI(uriToSend);
+        remoteWindow.openURI(uriToSend);
         return;
       } catch (e) {
         if (e.result != Cr.NS_NOINTERFACE) {
           throw e;
         }
       }
 
+      try {
+        let isPrivate = aWindowContext.getInterface(Ci.nsIDocShell)
+                                      .QueryInterface(Ci.nsILoadContext)
+                                      .usePrivateBrowsing;
+        if (isPrivate && !privateAllowed) {
+          throw Components.Exception("Extension not allowed in private windows.",
+                                      Cr.NS_ERROR_FILE_NOT_FOUND);
+        }
+      } catch (e) {
+        if (e.result != Cr.NS_NOINTERFACE) {
+          throw e;
+        }
+      }
+
       // create a channel from this URI
       var channel = NetUtil.newChannel({
         uri: uriToSend,
         loadUsingSystemPrincipal: true,
       });
       channel.loadFlags = Ci.nsIChannel.LOAD_DOCUMENT_URI;
 
       // load the channel
@@ -94,43 +116,47 @@ nsWebHandlerApp.prototype = {
       // default since browsers don't care much, and link click is likely to be
       // the more interesting case for non-browser apps.  See
       // <https://bugzilla.mozilla.org/show_bug.cgi?id=392957#c9> for details.
       uriLoader.openURI(channel, Ci.nsIURILoader.IS_CONTENT_PREFERRED,
                         aWindowContext);
       return;
     }
 
-    // get browser dom window
-    var browserDOMWin = Services.wm.getMostRecentWindow("navigator:browser")
-                                   .QueryInterface(Ci.nsIDOMChromeWindow)
-                                   .browserDOMWindow;
+    let win = Services.wm.getMostRecentWindow("navigator:browser");
 
-    // if we got an exception, there are several possible reasons why:
+    // If this is an extension handler, check private browsing access.
+    if (!privateAllowed &&
+        PrivateBrowsingUtils.isContentWindowPrivate(win)) {
+      throw Components.Exception("Extension not allowed in private windows.",
+                                 Cr.NS_ERROR_FILE_NOT_FOUND);
+    }
+
+    // If we get an exception, there are several possible reasons why:
     // a) this gecko embedding doesn't provide an nsIBrowserDOMWindow
     //    implementation (i.e. doesn't support browser-style functionality),
     //    so we need to kick the URL out to the OS default browser.  This is
     //    the subject of bug 394479.
     // b) this embedding does provide an nsIBrowserDOMWindow impl, but
     //    there doesn't happen to be a browser window open at the moment; one
     //    should be opened.  It's not clear whether this situation will really
     //    ever occur in real life.  If it does, the only API that I can find
     //    that seems reasonably likely to work for most embedders is the
     //    command line handler.
     // c) something else went wrong
     //
-    // it's not clear how one would differentiate between the three cases
-    // above, so for now we don't catch the exception
+    // It's not clear how one would differentiate between the three cases
+    // above, so for now we don't catch the exception.
 
     // openURI
-    browserDOMWin.openURI(uriToSend,
-                          null, // no window.opener
-                          Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW,
-                          Ci.nsIBrowserDOMWindow.OPEN_NEW,
-                          Services.scriptSecurityManager.getSystemPrincipal());
+    win.browserDOMWindow.openURI(uriToSend,
+                                 null, // no window.opener
+                                 Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW,
+                                 Ci.nsIBrowserDOMWindow.OPEN_NEW,
+                                 Services.scriptSecurityManager.getSystemPrincipal());
   },
 
   // nsIWebHandlerApp
 
   get uriTemplate() {
     return this._uriTemplate;
   },