Bug 1334550 - Part 2 - Add nsISubstitutionObserver and update test_extensionURL.html; r=jimm
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
--- a/caps/tests/mochitest/test_extensionURL.html
+++ b/caps/tests/mochitest/test_extensionURL.html
@@ -13,51 +13,166 @@ https://bugzilla.mozilla.org/show_bug.cg
/** Test for Bug 1161831 **/
SimpleTest.waitForExplicitFinish();
let module = SpecialPowers.Cu.import("resource://gre/modules/Services.jsm", {});
var {MatchGlob, MatchPatternSet, WebExtensionPolicy} = module;
var policy1, policy2;
+ var XPCOMUtils = SpecialPowers.Cu.import("resource://gre/modules/XPCOMUtils.jsm").XPCOMUtils;
var resourceHandler = SpecialPowers.Services.io.getProtocolHandler("resource")
.QueryInterface(SpecialPowers.Ci.nsISubstitutingProtocolHandler);
var extensionHandler = SpecialPowers.Services.io.getProtocolHandler("moz-extension")
.QueryInterface(SpecialPowers.Ci.nsISubstitutingProtocolHandler);
+ var fileHandler = SpecialPowers.Cc["@mozilla.org/network/protocol;1?name=file"]
+ .getService(SpecialPowers.Ci.nsIFileProtocolHandler);
+
+ // Chrome script that adds handles for inserting substitutions and
+ // resolving symlinked paths in the parent process.
+ var script = SpecialPowers.loadChromeScript(() => {
+ const Ci = Components.interfaces;
+ const Cc = Components.classes;
+ const Cu = Components.utils;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Sets up a substitution in the parent process
+ this.addMessageListener("SetSubstitution", ({from, to}) => {
+ // Convert the passed |to| string to a URI.
+ // A null |to| value clears the substitution
+ if (to != null) {
+ var uri = Services.io.newURI(to);
+ }
+ Services.io.getProtocolHandler("moz-extension")
+ .QueryInterface(Ci.nsISubstitutingProtocolHandler)
+ .setSubstitution(from, uri);
+ });
+
+ // Gets a normalized (de-symlinked) path in the parent process
+ this.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 callback and a host
+ // for a substitution that has been set in either the parent
+ // or local child process and for which we are waiting for the
+ // observer notification in the child process.
+ var pendingSubstitutions = [];
+
+ // Adds a new callback to |pendingSubstitutions|.
+ function pushSubstitutionCallback(callback, host) {
+ let entry = {callback, host};
+ pendingSubstitutions.push(entry);
+ }
+
+ // Invoke the first callback found in |pendingSubstitutions|
+ // with a matching host.
+ function popSubstitutionCallback(host) {
+ for (let i = 0; i < pendingSubstitutions.length; i++) {
+ let entry = pendingSubstitutions[i];
+ if (host === entry.host) {
+ entry.callback();
+ pendingSubstitutions.splice(i, 1);
+ return;
+ }
+ }
+ // This indicates we installed a mapping in either the
+ // parent or the local child process, but never received an
+ // observer notification in the child for a mapping with
+ // a matching host.
+ ok(false, `popSubstitutionCallback(${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) {
+ popSubstitutionCallback(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(chromeScript, from, to) {
+ var p = new Promise(function(resolve, reject) {
+ pushSubstitutionCallback(resolve, from);
+ chromeScript.sendSyncMessage("SetSubstitution", {from, to});
+ });
+ return p;
+ }
SimpleTest.registerCleanupFunction(function() {
+ extensionHandler.removeObserver(wrappedObserver);
policy1.active = false;
policy2.active = false;
+ script.sendSyncMessage("SetSubstitution", {from: "cherise", to: null});
+ script.sendSyncMessage("SetSubstitution", {from: "liebchen", to: null});
});
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 filePath = resourceHandler.resolveURI(resURI);
ok(filePath.startsWith("file://"), "resource:// URI resolves where we expect: " + filePath);
- var fileURI = SpecialPowers.Services.io.newURI(filePath);
+ var resFile = fileHandler.getFileFromURLSpec(filePath);
+
+ // Get normalized path to the test file. We already have a file object
+ // |resFile|, but its underlying path may contain a symlink we can't
+ // resolve in the child process.
+ let resolvedPath = script.sendSyncMessage("ResolvePath", resFile.path);
+ let file = SpecialPowers.Cc["@mozilla.org/file/local;1"]
+ .createInstance(SpecialPowers.Ci.nsILocalFile);
+ info(`resolved test file path: ${resolvedPath}`);
+ 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}`);
function StubPolicy(id, accessible) {
let policy = new WebExtensionPolicy(SpecialPowers.Cu.cloneInto({
id: `imaginaryaddon-${id[0]}`,
mozExtensionHostname: id,
- baseURL: fileURI.spec,
+ baseURL: cheriseBaseDirURIStr,
allowedOrigins: SpecialPowers.unwrap(new MatchPatternSet([])),
webAccessibleResources: accessible ? [SpecialPowers.unwrap(new MatchGlob("*"))] : [],
localizeCallback(string) {},
}, module, {cloneFunctions: true, wrapReflectors: true}));
+ // Activating the policy results in a substitution being added,
+ // which triggers SubstitutionObserver.onSetSubstitution().
+ // All observer notifications must be accounted for (in this
+ // test to validate they are working correctly) so ignore this
+ // substitution when the observer gets notified.
+ pushSubstitutionCallback(() => {}, id);
policy.active = true;
return policy;
}
- // Register a moz-extension:// URI.
+ // Register a moz-extension:// URI locally.
policy1 = StubPolicy("cherise", false);
policy2 = StubPolicy("liebchen", false);
//
// Make sure that non-file:// URIs don't work.
//
// resource://
@@ -84,17 +199,20 @@ https://bugzilla.mozilla.org/show_bug.cg
SpecialPowers.wrap(ifr).contentWindow
.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
.getInterface(SpecialPowers.Ci.nsIWebNavigation)
.loadURI(url, 0, null, null, null);
}
function setWhitelistCallback(paths) {
+ pushSubstitutionCallback(() => {}, policy1.mozExtensionHostname);
policy1.active = false;
+
+ pushSubstitutionCallback(() => {}, policy2.mozExtensionHostname);
policy2.active = false;
policy1 = StubPolicy("cherise", paths.includes("cherise"));
policy2 = StubPolicy("liebchen", paths.includes("liebchen"));
}
function testLoad(url, navigate, shouldThrow) {
var ifr = document.createElement("iframe");
@@ -142,29 +260,31 @@ https://bugzilla.mozilla.org/show_bug.cg
xhr.open("GET", url, true);
xhr.send();
});
}
//
// 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, 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>
--- a/netwerk/protocol/res/SubstitutingProtocolHandler.cpp
+++ b/netwerk/protocol/res/SubstitutingProtocolHandler.cpp
@@ -292,44 +292,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);
@@ -403,10 +406,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;