Bug 1334550 - Part 2 - Add nsISubstitutionObserver and update test_extensionURL.html; r?kmag draft
authorHaik Aftandilian <haftandilian@mozilla.com>
Fri, 12 May 2017 10:50:30 -0700
changeset 577257 e353630f3ca90ce2a9224c6d508b16a009c7936c
parent 577153 afbba6de20ea62c4294e1aad298dc21e1447ac7e
child 628465 8cfbee01914838ad9a20a41da10965657d7825c4
push id58652
push userhaftandilian@mozilla.com
push dateSat, 13 May 2017 03:36:00 +0000
reviewerskmag
bugs1334550
milestone55.0a1
Bug 1334550 - Part 2 - Add nsISubstitutionObserver and update test_extensionURL.html; r?kmag Adds nsISubstitutionObserver so that substitutions set on a parent nsISubstitutingProtocolHandler which are then propagated to child processes can be observed in the child. Updates test_extensionURL.html to set substitutions on the parent ExtensionProtocolHandler before trying to load moz-extension URI's using those substitutions. MozReview-Commit-ID: JaW1A3uZpoO
caps/tests/mochitest/test_extensionURL.html
netwerk/protocol/res/SubstitutingProtocolHandler.cpp
netwerk/protocol/res/SubstitutingProtocolHandler.h
netwerk/protocol/res/moz.build
netwerk/protocol/res/nsISubstitutingProtocolHandler.idl
netwerk/protocol/res/nsISubstitutionObserver.idl
netwerk/protocol/res/nsResProtocolHandler.h
--- a/caps/tests/mochitest/test_extensionURL.html
+++ b/caps/tests/mochitest/test_extensionURL.html
@@ -8,103 +8,180 @@ https://bugzilla.mozilla.org/show_bug.cg
   <title>Test for Bug 1161831</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <script type="application/javascript">
 
   /** Test for Bug 1161831 **/
   SimpleTest.waitForExplicitFinish();
 
+  var XPCOMUtils = SpecialPowers.Cu.import("resource://gre/modules/XPCOMUtils.jsm").XPCOMUtils;
   var aps = SpecialPowers.Cc["@mozilla.org/addons/policy-service;1"]
                          .getService(SpecialPowers.Ci.nsIAddonPolicyService).wrappedJSObject;
   var oldLoadCallback = aps.setExtensionURILoadCallback(null);
   var oldMapCallback = aps.setExtensionURIToAddonIdCallback(null);
   var resourceHandler = SpecialPowers.Services.io.getProtocolHandler("resource")
                                      .QueryInterface(SpecialPowers.Ci.nsISubstitutingProtocolHandler);
   var extensionHandler = SpecialPowers.Services.io.getProtocolHandler("moz-extension")
                                      .QueryInterface(SpecialPowers.Ci.nsISubstitutingProtocolHandler);
 
+  var script = SpecialPowers.loadChromeScript(() => {
+    const Ci = Components.interfaces;
+    const Cc = Components.classes;
+    const Cu = Components.utils;
+    Cu.import("resource://gre/modules/Services.jsm");
+
+    addMessageListener("SetSubstitution", ({from, to}) => {
+      // Convert the passed |to| string to a URI.
+      // Pass through null |to| values as a null
+      // uri is used to clear the substitution
+      if (to != null) {
+        var uri = Services.io.newURI(to);
+      }
+      var extensionHandler = Services.io.getProtocolHandler("moz-extension")
+                             .QueryInterface(Ci.nsISubstitutingProtocolHandler);
+      extensionHandler.setSubstitution(from, uri);
+    });
+    addMessageListener("ResolvePath", (path) => {
+      let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+      file.initWithPath(path);
+      file.normalize();
+      return file.path;
+    });
+  });
+
+  // An array of objects each containing a promise resolver
+  // function and a host for a substitution that has been set
+  // in the parent process and for which we are waiting to be
+  // observed in the child process.
+  var pendingSubstitutions = [];
+
+  // Adds a new entry to |pendingSubstitutions|.
+  function waitForSubstitution(resolver, host) {
+    let entry = {resolver: resolver, host: host};
+    pendingSubstitutions.push(entry);
+  }
+
+  // Resolve the first promise found in |pendingSubstitutions|
+  // with a matching host.
+  function resolveSubPromise(host) {
+    for (let i = 0; i < pendingSubstitutions.length; i++) {
+      let entry = pendingSubstitutions[i];
+      if (host === entry.host) {
+        entry.resolver();
+        pendingSubstitutions.splice(i, 1);
+        return;
+      }
+    }
+    // This indicates we installed a mapping in the parent,
+    // but never received an observer notification in the
+    // child for a mapping with a matching host.
+    ok(false, `resolveSubPromise(${host}) no match found`);
+  }
+
+  // Define an implementation of nsISubstitutionObserver and add it
+  // to this process' ExtensionProtocolHandler to observe substitutions
+  // as they are propagated to the child.
+  function SubstitutionObserver() {}
+  SubstitutionObserver.prototype = {
+    onSetSubstitution: SpecialPowers.wrapCallback(function(root, uri) {
+      spec = (uri === null) ? "null" : uri.spec;
+      resolveSubPromise(root);
+    }),
+    QueryInterface:
+      XPCOMUtils.generateQI([SpecialPowers.Ci.nsISupports,
+          SpecialPowers.Ci.nsISubstitutionObserver]),
+  };
+  var observer = new SubstitutionObserver();
+  var wrappedObserver = SpecialPowers.wrap(observer);
+  extensionHandler.addObserver(wrappedObserver);
+
+  // Set a substitution in the parent. The parent
+  // propagates all substitutions to child processes.
+  function globalSetSubstitution(script, from, to) {
+    var p = new Promise(function(resolve, reject) {
+      waitForSubstitution(resolve, from);
+      script.sendSyncMessage('SetSubstitution', {from: from, to: to});
+    });
+    return p;
+  }
+
   SimpleTest.registerCleanupFunction(function() {
-      extensionHandler.setSubstitution("cherise", null);
-      extensionHandler.setSubstitution("liebchen", null);
+      extensionHandler.removeObserver(wrappedObserver);
+      script.sendSyncMessage('SetSubstitution', {from: 'cherise', to: null});
+      script.sendSyncMessage('SetSubstitution', {from: 'liebchen', to: null});
       aps.setExtensionURILoadCallback(oldLoadCallback);
       aps.setExtensionURIToAddonIdCallback(oldMapCallback);
   });
 
   addLoadEvent(function() {
-
     // First, get a file:// URI to something - open to suggestions on how to do
     //  this more easily.
-    var resURI = SpecialPowers.Services.io.newURI("resource://testing-common/resource_test_file.html");
+
+    var resURI = SpecialPowers.Services.io.newURI('resource://testing-common/resource_test_file.html');
     var filePath = resourceHandler.resolveURI(resURI);
-    ok(filePath.startsWith("file://"), "resource:// URI resolves where we expect: " + filePath);
+    ok(filePath.startsWith('file://'), 'resource:// URI resolves where we expect: ' + filePath);
     var fileURI = SpecialPowers.Services.io.newURI(filePath);
 
-    // Register a moz-extension:// URI.
-    extensionHandler.setSubstitution("cherise", fileURI);
-
-    // Alias the above.
-    extensionHandler.setSubstitution("liebchen", SpecialPowers.Services.io.newURI("moz-extension://cherise"));
-
     //
     // Make sure that non-file:// URIs don't work.
     //
 
     // resource://
     try {
-      extensionHandler.setSubstitution("interdit", resURI);
+      extensionHandler.setSubstitution('interdit', resURI);
       ok(false, "Should have thrown for mapping moz-extension to resource");
     } catch (e) {
       ok(true, "Threw correctly: " + e);
     }
 
     // chrome://
     try {
-      var chromeURI = SpecialPowers.Services.io.newURI("chrome://global/content/mozilla.xhtml");
-      extensionHandler.setSubstitution("verboten", chromeURI);
+      var chromeURI = SpecialPowers.Services.io.newURI('chrome://global/content/mozilla.xhtml');
+      extensionHandler.setSubstitution('verboten', chromeURI);
       ok(false, "Should have thrown for mapping moz-extension to chrome");
     } catch (e) {
       ok(true, "Threw correctly: " + e);
     }
 
     function navigateWithLocation(ifr, url) { ifr.contentWindow.location = url; }
-    function navigateWithSrc(ifr, url) { ifr.setAttribute("src", url); }
+    function navigateWithSrc(ifr, url) { ifr.setAttribute('src', url); }
     function navigateFromChromeWithLocation(ifr, url) { SpecialPowers.wrap(ifr).contentWindow.location = url; }
     function navigateFromChromeWithWebNav(ifr, url) {
       SpecialPowers.wrap(ifr).contentWindow
                    .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
                    .getInterface(SpecialPowers.Ci.nsIWebNavigation)
                    .loadURI(url, 0, null, null, null);
     }
 
 
     function setWhitelistCallback(rgxp) {
       var cb = SpecialPowers.wrapCallback(function(uri) { return rgxp.test(uri.spec); });
       aps.setExtensionURILoadCallback(cb);
     }
 
-    aps.setExtensionURIToAddonIdCallback(SpecialPowers.wrapCallback(function(uri) { return "imaginaryaddon-" + uri.host[0]; }));
+    aps.setExtensionURIToAddonIdCallback(SpecialPowers.wrapCallback(function (uri) { return 'imaginaryaddon-' + uri.host[0]; }));
 
     function testLoad(url, navigate, shouldThrow) {
-      var ifr = document.createElement("iframe");
+      var ifr = document.createElement('iframe');
       var p = new Promise(function(resolve, reject) {
         ifr.onload = function() {
-          ok(true, "Loaded " + url);
+          ok(true, 'Loaded ' + url);
           var prin = SpecialPowers.wrap(ifr.contentWindow).document.nodePrincipal;
-          function stripTrailingSlash(s) { return s.replace(/\/$/, ""); }
-          is(stripTrailingSlash(prin.URI.spec), url, "Principal uri is correct: " + url);
-          function stripPath(s) { return s.replace(/(.*\/\/.+)\/.*/, "$1"); }
-          is(prin.originNoSuffix, stripPath(url), "Principal origin is correct: " + prin.originNoSuffix);
-          is(prin.addonId, "imaginaryaddon-" + url[url.indexOf("/") + 2], "addonId is correct");
+          function stripTrailingSlash(s) { return s.replace(/\/$/, ''); };
+          is(stripTrailingSlash(prin.URI.spec), url, 'Principal uri is correct: ' + url);
+          function stripPath(s) { return s.replace(/(.*\/\/.+)\/.*/, '$1'); };
+          is(prin.originNoSuffix, stripPath(url), 'Principal origin is correct: ' + prin.originNoSuffix);
+          is(prin.addonId, 'imaginaryaddon-' + url[url.indexOf('/') + 2], 'addonId is correct');
           if (/_blank/.test(url)) {
             is(SpecialPowers.wrap(ifr.contentWindow).document.documentElement.innerHTML,
-               "<head></head><body></body>", "blank document looks right");
+               '<head></head><body></body>', 'blank document looks right');
           } else {
-            is(SpecialPowers.wrap(ifr.contentWindow).document.title, "resource test file",
-               "document looks right");
+            is(SpecialPowers.wrap(ifr.contentWindow).document.title, 'resource test file',
+              'document looks right');
           }
           ifr.remove();
           resolve();
         };
         document.body.appendChild(ifr);
 
         var threw = false;
         try {
@@ -112,47 +189,63 @@ https://bugzilla.mozilla.org/show_bug.cg
         } catch (e) {
           ifr.remove();
           threw = true;
           ok(/denied|insecure/.test(e), "exception correct: " + e);
         }
         is(threw, !!shouldThrow, "Correct throwing behavior for: " + url);
         !threw || resolve();
       });
-
       return p;
     }
 
     function testXHR(url, shouldError) {
       return new Promise(function(resolve, reject) {
         var xhr = new XMLHttpRequest();
         xhr.addEventListener("load", () => { ok(!shouldError, `XHR to ${url} should succeed`); resolve(); });
         xhr.addEventListener("error", () => { ok(shouldError, `XHR to ${url} should fail`); resolve(); });
         xhr.open("GET", url, true);
         xhr.send();
       });
     }
 
+    // Get normalized path to the directory containing the test file
+    let resolvedPath = script.sendSyncMessage('ResolvePath', fileURI.filePath);
+    let file = SpecialPowers.Cc["@mozilla.org/file/local;1"]
+                            .createInstance(SpecialPowers.Ci.nsILocalFile);
+    file.initWithPath(resolvedPath);
+
+    // Setup the base directory URI string and a URI string to refer to
+    // the test file within that directory.
+    let cheriseURIStr = 'moz-extension://cherise/' + file.leafName;
+    let liebchenURIStr = 'moz-extension://liebchen/' + file.leafName;
+    let cheriseBaseDirURIStr = 'file://' + file.parent.path + '/';
+    info(`cheriseURIStr: ${cheriseURIStr}`);
+    info(`liebchenURIStr: ${liebchenURIStr}`);
+    info(`cheriseBaseDirURIStr: ${cheriseBaseDirURIStr}`);
+
     //
     // Perform some loads and make sure they work correctly.
     //
-    testLoad.bind(null, "moz-extension://cherise", navigateFromChromeWithLocation)()
-    .then(testLoad.bind(null, "moz-extension://cherise", navigateFromChromeWithWebNav))
-    .then(testLoad.bind(null, "moz-extension://cherise", navigateWithLocation, /* shouldThrow = */ true))
-    .then(testXHR.bind(null, "moz-extension://cherise", /* shouldError = */ true))
+    globalSetSubstitution(script, 'cherise', cheriseBaseDirURIStr)
+    .then(testLoad.bind(null, cheriseURIStr, navigateFromChromeWithLocation))
+    .then(testLoad.bind(null, cheriseURIStr, navigateFromChromeWithWebNav))
+    .then(testLoad.bind(null, cheriseURIStr, navigateWithLocation, /* shouldThrow = */ true))
+    .then(testXHR.bind(null, cheriseURIStr, /* shouldError = */ true))
     .then(setWhitelistCallback.bind(null, /cherise/))
-    .then(testLoad.bind(null, "moz-extension://cherise", navigateWithLocation))
-    .then(testXHR.bind(null, "moz-extension://cherise"))
-    .then(testLoad.bind(null, "moz-extension://liebchen", navigateWithLocation, /* shouldThrow = */ true))
-    .then(testXHR.bind(null, "moz-extension://liebchen", /* shouldError = */ true))
+    .then(testLoad.bind(null, cheriseURIStr, navigateWithLocation))
+    .then(testXHR.bind(null, cheriseURIStr))
+    .then(globalSetSubstitution(script, 'liebchen', 'moz-extension://cherise'))
+    .then(testLoad.bind(null, liebchenURIStr, navigateWithLocation, /* shouldThrow = */ true))
+    .then(testXHR.bind(null, liebchenURIStr, /* shouldError = */ true))
     .then(setWhitelistCallback.bind(null, /cherise|liebchen/))
-    .then(testLoad.bind(null, "moz-extension://liebchen", navigateWithLocation))
-    .then(testLoad.bind(null, "moz-extension://liebchen", navigateWithSrc))
-    .then(testLoad.bind(null, "moz-extension://cherise", navigateWithSrc))
-    .then(testLoad.bind(null, "moz-extension://cherise/_blank.html", navigateWithSrc))
+    .then(testLoad.bind(null, liebchenURIStr, navigateWithLocation))
+    .then(testLoad.bind(null, liebchenURIStr, navigateWithSrc))
+    .then(testLoad.bind(null, cheriseURIStr, navigateWithSrc))
+    .then(testLoad.bind(null, 'moz-extension://cherise/_blank.html', navigateWithSrc))
     .then(SimpleTest.finish.bind(SimpleTest),
           function(e) { ok(false, "rejected promise: " + e); SimpleTest.finish() }
     );
   });
 
   </script>
 </head>
 <body>
--- a/netwerk/protocol/res/SubstitutingProtocolHandler.cpp
+++ b/netwerk/protocol/res/SubstitutingProtocolHandler.cpp
@@ -293,44 +293,47 @@ SubstitutingProtocolHandler::AllowPort(i
 // nsISubstitutingProtocolHandler
 //----------------------------------------------------------------------------
 
 nsresult
 SubstitutingProtocolHandler::SetSubstitution(const nsACString& root, nsIURI *baseURI)
 {
   if (!baseURI) {
     mSubstitutions.Remove(root);
+    NotifyObservers(root, baseURI);
     return SendSubstitution(root, baseURI);
   }
 
   // If baseURI isn't a same-scheme URI, we can set the substitution immediately.
   nsAutoCString scheme;
   nsresult rv = baseURI->GetScheme(scheme);
   NS_ENSURE_SUCCESS(rv, rv);
   if (!scheme.Equals(mScheme)) {
     if (mEnforceFileOrJar && !scheme.EqualsLiteral("file") && !scheme.EqualsLiteral("jar")
         && !scheme.EqualsLiteral("app")) {
       NS_WARNING("Refusing to create substituting URI to non-file:// target");
       return NS_ERROR_INVALID_ARG;
     }
 
     mSubstitutions.Put(root, baseURI);
+    NotifyObservers(root, baseURI);
     return SendSubstitution(root, baseURI);
   }
 
   // baseURI is a same-type substituting URI, let's resolve it first.
   nsAutoCString newBase;
   rv = ResolveURI(baseURI, newBase);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIURI> newBaseURI;
   rv = mIOService->NewURI(newBase, nullptr, nullptr, getter_AddRefs(newBaseURI));
   NS_ENSURE_SUCCESS(rv, rv);
 
   mSubstitutions.Put(root, newBaseURI);
+  NotifyObservers(root, baseURI);
   return SendSubstitution(root, newBaseURI);
 }
 
 nsresult
 SubstitutingProtocolHandler::GetSubstitution(const nsACString& root, nsIURI **result)
 {
   NS_ENSURE_ARG_POINTER(result);
 
@@ -404,10 +407,43 @@ SubstitutingProtocolHandler::ResolveURI(
   if (MOZ_LOG_TEST(gResLog, LogLevel::Debug)) {
     nsAutoCString spec;
     uri->GetAsciiSpec(spec);
     MOZ_LOG(gResLog, LogLevel::Debug, ("%s\n -> %s\n", spec.get(), PromiseFlatCString(result).get()));
   }
   return rv;
 }
 
+nsresult
+SubstitutingProtocolHandler::AddObserver(nsISubstitutionObserver* aObserver)
+{
+  NS_ENSURE_ARG(aObserver);
+  if (mObservers.Contains(aObserver)) {
+    return NS_ERROR_DUPLICATE_HANDLE;
+  }
+
+  mObservers.AppendElement(aObserver);
+  return NS_OK;
+}
+
+nsresult
+SubstitutingProtocolHandler::RemoveObserver(nsISubstitutionObserver* aObserver)
+{
+  NS_ENSURE_ARG(aObserver);
+  if (!mObservers.Contains(aObserver)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  mObservers.RemoveElement(aObserver);
+  return NS_OK;
+}
+
+void
+SubstitutingProtocolHandler::NotifyObservers(const nsACString& aRoot,
+                                             nsIURI* aBaseURI)
+{
+  for (size_t i = 0; i < mObservers.Length(); ++i) {
+    mObservers[i]->OnSetSubstitution(aRoot, aBaseURI);
+  }
+}
+
 } // namespace net
 } // namespace mozilla
--- a/netwerk/protocol/res/SubstitutingProtocolHandler.h
+++ b/netwerk/protocol/res/SubstitutingProtocolHandler.h
@@ -6,16 +6,17 @@
 
 #ifndef SubstitutingProtocolHandler_h___
 #define SubstitutingProtocolHandler_h___
 
 #include "nsISubstitutingProtocolHandler.h"
 
 #include "nsInterfaceHashtable.h"
 #include "nsIOService.h"
+#include "nsISubstitutionObserver.h"
 #include "nsStandardURL.h"
 #include "mozilla/chrome/RegistryMessageUtils.h"
 #include "mozilla/Maybe.h"
 
 class nsIIOService;
 
 namespace mozilla {
 namespace net {
@@ -68,21 +69,29 @@ protected:
   virtual MOZ_MUST_USE nsresult SubstituteChannel(nsIURI* uri, nsILoadInfo* aLoadInfo, nsIChannel** result)
   {
     return NS_OK;
   }
 
   nsIIOService* IOService() { return mIOService; }
 
 private:
+  // Notifies all observers that a new substitution from |aRoot| to
+  // |aBaseURI| has been set/installed for this protocol handler.
+  void NotifyObservers(const nsACString& aRoot, nsIURI* aBaseURI);
+
   nsCString mScheme;
   Maybe<uint32_t> mFlags;
   nsInterfaceHashtable<nsCStringHashKey,nsIURI> mSubstitutions;
   nsCOMPtr<nsIIOService> mIOService;
 
+  // The list of observers added with AddObserver that will be
+  // notified when substitutions are set or unset.
+  nsTArray<nsCOMPtr<nsISubstitutionObserver>> mObservers;
+
   // In general, we expect the principal of a document loaded from a
   // substituting URI to be a codebase principal for that URI (rather than
   // a principal for whatever is underneath). However, this only works if
   // the protocol handler for the underlying URI doesn't set an explicit
   // owner (which chrome:// does, for example). So we want to require that
   // substituting URIs only map to other URIs of the same type, or to
   // file:// and jar:// URIs.
   //
--- a/netwerk/protocol/res/moz.build
+++ b/netwerk/protocol/res/moz.build
@@ -2,16 +2,17 @@
 # vim: set filetype=python:
 # 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/.
 
 XPIDL_SOURCES += [
     'nsIResProtocolHandler.idl',
     'nsISubstitutingProtocolHandler.idl',
+    'nsISubstitutionObserver.idl',
 ]
 
 XPIDL_MODULE = 'necko_res'
 
 EXPORTS.mozilla.net += [
     'ExtensionProtocolHandler.h',
     'SubstitutingProtocolHandler.h',
 ]
--- a/netwerk/protocol/res/nsISubstitutingProtocolHandler.idl
+++ b/netwerk/protocol/res/nsISubstitutingProtocolHandler.idl
@@ -1,15 +1,17 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
 #include "nsIProtocolHandler.idl"
 
+interface nsISubstitutionObserver;
+
 /**
  * Protocol handler superinterface for a protocol which performs substitutions
  * from URIs of its scheme to URIs of another scheme.
  */
 [scriptable, uuid(154c64fd-a69e-4105-89f8-bd7dfe621372)]
 interface nsISubstitutingProtocolHandler : nsIProtocolHandler
 {
   /**
@@ -38,9 +40,23 @@ interface nsISubstitutingProtocolHandler
   /**
    * Utility function to resolve a substituted URI.  A resolved URI is not
    * guaranteed to reference a resource that exists (ie. opening a channel to
    * the resolved URI may fail).
    *
    * @throws NS_ERROR_NOT_AVAILABLE if resURI.host() is an unknown root key.
    */
   [must_use] AUTF8String resolveURI(in nsIURI resURI);
+
+  /**
+   * Adds an observer that will be notified on the main thread whenever a
+   * substitition is set or unset. Notifications are not sent for substitutions
+   * that were set prior to the observer being added. Holds an owning reference
+   * to the observer until removeObserver is called or the protocol handler is
+   * destroyed.
+   */
+  [must_use] void addObserver(in nsISubstitutionObserver observer);
+
+  /**
+   * Removes the observer.
+   */
+  [must_use] void removeObserver(in nsISubstitutionObserver observer);
 };
new file mode 100644
--- /dev/null
+++ b/netwerk/protocol/res/nsISubstitutionObserver.idl
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+
+/**
+ * An observer of substitutions being set or unset on a
+ * SubstitutingProtocolHandler. Useful for receiving asynchronous notification
+ * in a child process after a substitution has been set in the parent process
+ * and is propagated to the child.
+ */
+[scriptable, uuid(492c9192-3803-4e2b-8373-d25fe55f5588)]
+interface nsISubstitutionObserver : nsISupports
+{
+    /**
+     * To be called when a substition has been set or unset on a protocol
+     * handler. Unset operations are identified by a null URI argument.
+     *
+     * @param aRoot the root key of the mapping
+     * @param aBaseURI the base URI to be substituted for the root key by the
+     *        protocol handler. For notifications triggered by unset
+     *        operations (i.e., when is a substitution is removed from the
+     *        protocol handler) this argument is null.
+     */
+    void onSetSubstitution(in ACString aRoot, in nsIURI aBaseURI);
+};
--- a/netwerk/protocol/res/nsResProtocolHandler.h
+++ b/netwerk/protocol/res/nsResProtocolHandler.h
@@ -8,16 +8,18 @@
 
 #include "SubstitutingProtocolHandler.h"
 
 #include "nsIResProtocolHandler.h"
 #include "nsInterfaceHashtable.h"
 #include "nsWeakReference.h"
 #include "nsStandardURL.h"
 
+class nsISubstitutionObserver;
+
 struct SubstitutionMapping;
 class nsResProtocolHandler final : public nsIResProtocolHandler,
                                    public mozilla::SubstitutingProtocolHandler,
                                    public nsSupportsWeakReference
 {
 public:
     NS_DECL_ISUPPORTS_INHERITED
     NS_DECL_NSIRESPROTOCOLHANDLER
@@ -43,16 +45,26 @@ public:
         return mozilla::SubstitutingProtocolHandler::HasSubstitution(aRoot, aResult);
     }
 
     NS_IMETHOD ResolveURI(nsIURI *aResURI, nsACString& aResult) override
     {
         return mozilla::SubstitutingProtocolHandler::ResolveURI(aResURI, aResult);
     }
 
+    NS_IMETHOD AddObserver(nsISubstitutionObserver *aObserver) override
+    {
+        return mozilla::SubstitutingProtocolHandler::AddObserver(aObserver);
+    }
+
+    NS_IMETHOD RemoveObserver(nsISubstitutionObserver *aObserver) override
+    {
+        return mozilla::SubstitutingProtocolHandler::RemoveObserver(aObserver);
+    }
+
 protected:
     MOZ_MUST_USE nsresult GetSubstitutionInternal(const nsACString& aRoot, nsIURI** aResult) override;
     virtual ~nsResProtocolHandler() {}
 
     MOZ_MUST_USE bool ResolveSpecialCases(const nsACString& aHost,
                                           const nsACString& aPath,
                                           const nsACString& aPathname,
                                           nsACString& aResult) override;