Bug 1322235: Part 4 - Add initial native WebExtensionPolicy bindings. r=billm,aswan
authorKris Maglione <maglione.k@gmail.com>
Wed, 24 May 2017 15:58:19 -0700
changeset 410369 19d92b19910f3ce9bfa2b2874d17635fe323080b
parent 410368 34316bd2fecfb2802b135ea4f8592468c5fc7618
child 410370 02ce614daf1999405cd4e63d60ab2b6ebd4e4048
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm, aswan
bugs1322235
milestone55.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 1322235: Part 4 - Add initial native WebExtensionPolicy bindings. r=billm,aswan Bill, can you please review the binding code and the general sanity of the platform code? Andrew and zombie, can you please review the policy logic and tests? As in part 1, this aims to reduce the overhead of our extension policy logic by making it directly available to native code with as little JS and XPConnect overhead as possible. MozReview-Commit-ID: 40m1wSEYtBo
dom/bindings/Bindings.conf
dom/webidl/WebExtensionPolicy.webidl
dom/webidl/moz.build
toolkit/components/extensions/WebExtensionPolicy.cpp
toolkit/components/extensions/WebExtensionPolicy.h
toolkit/components/extensions/moz.build
toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -1096,16 +1096,20 @@ DOMInterfaces = {
 'VTTRegion': {
   'nativeType': 'mozilla::dom::TextTrackRegion',
 },
 
 'WebAuthentication': {
     'implicitJSContext': 'makeCredential',
 },
 
+'WebExtensionPolicy': {
+    'nativeType': 'mozilla::extensions::WebExtensionPolicy',
+},
+
 'WindowClient': {
     'nativeType': 'mozilla::dom::workers::ServiceWorkerWindowClient',
     'headerFile': 'mozilla/dom/workers/bindings/ServiceWorkerWindowClient.h',
 },
 
 'WebGLActiveInfo': {
     'nativeType': 'mozilla::WebGLActiveInfo',
     'headerFile': 'WebGLActiveInfo.h'
new file mode 100644
--- /dev/null
+++ b/dom/webidl/WebExtensionPolicy.webidl
@@ -0,0 +1,121 @@
+/* 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/. */
+
+interface URI;
+interface WindowProxy;
+
+callback WebExtensionLocalizeCallback = DOMString (DOMString unlocalizedText);
+
+/**
+ * Defines the platform-level policies for a WebExtension, including its
+ * permissions and the characteristics of its moz-extension: URLs.
+ */
+[Constructor(WebExtensionInit options), ChromeOnly, Exposed=System]
+interface WebExtensionPolicy {
+  /**
+   * The add-on's internal ID, as specified in its manifest.json file or its
+   * XPI signature.
+   */
+  [Constant, StoreInSlot]
+  readonly attribute DOMString id;
+
+  /**
+   * The hostname part of the extension's moz-extension: URLs. This value is
+   * generated randomly at install time.
+   */
+  [Constant, StoreInSlot]
+  readonly attribute ByteString mozExtensionHostname;
+
+  /**
+   * The file: or jar: URL to use for the base of the extension's
+   * moz-extension: URL root.
+   */
+  [Constant]
+  readonly attribute ByteString baseURL;
+
+  /**
+   * The content security policy string to apply to all pages loaded from the
+   * extension's moz-extension: protocol.
+   */
+  [Constant]
+  readonly attribute DOMString contentSecurityPolicy;
+
+
+  /**
+   * The list of currently-active permissions for the extension, as specified
+   * in its manifest.json file. May be updated to reflect changes in the
+   * extension's optional permissions.
+   */
+  [Cached, Frozen, Pure]
+  attribute sequence<DOMString> permissions;
+
+  /**
+   * Match patterns for the set of web origins to which the extension is
+   * currently allowed access. May be updated to reflect changes in the
+   * extension's optional permissions.
+   */
+  [Pure]
+  attribute MatchPatternSet allowedOrigins;
+
+  /**
+   * True if the extension is currently active, false otherwise. When active,
+   * the extension's moz-extension: protocol will point to the given baseURI,
+   * and the set of policies for this object will be active for its ID.
+   *
+   * Only one extension policy with a given ID or hostname may be active at a
+   * time. Attempting to activate a policy while a conflicting policy is
+   * active will raise an error.
+   */
+  [Affects=Everything, SetterThrows]
+  attribute boolean active;
+
+
+  /**
+   * Returns true if the extension has cross-origin access to the given URI.
+   */
+  boolean canAccessURI(URI uri, optional boolean explicit = false);
+
+  /**
+   * Returns true if the extension currently has the given permission.
+   */
+  boolean hasPermission(DOMString permission);
+
+  /**
+   * Returns true if the given path relative to the extension's moz-extension:
+   * URL root may be accessed by web content.
+   */
+  boolean isPathWebAccessible(DOMString pathname);
+
+  /**
+   * Replaces localization placeholders in the given string with localized
+   * text from the extension's currently active locale.
+   */
+  DOMString localize(DOMString unlocalizedText);
+
+  /**
+   * Returns the moz-extension: URL for the given path.
+   */
+  [Throws]
+  DOMString getURL(optional DOMString path = "");
+};
+
+dictionary WebExtensionInit {
+  required DOMString id;
+
+  required ByteString mozExtensionHostname;
+
+  required DOMString baseURL;
+
+  required WebExtensionLocalizeCallback localizeCallback;
+
+  required MatchPatternSet allowedOrigins;
+
+  sequence<DOMString> permissions = [];
+
+  sequence<MatchGlob> webAccessibleResources = [];
+
+  DOMString? contentSecurityPolicy = null;
+
+  sequence<DOMString>? backgroundScripts = null;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -939,16 +939,17 @@ WEBIDL_FILES = [
     'VRDisplay.webidl',
     'VRDisplayEvent.webidl',
     'VRServiceTest.webidl',
     'VTTCue.webidl',
     'VTTRegion.webidl',
     'WaveShaperNode.webidl',
     'WebAuthentication.webidl',
     'WebComponents.webidl',
+    'WebExtensionPolicy.webidl',
     'WebGL2RenderingContext.webidl',
     'WebGLRenderingContext.webidl',
     'WebKitCSSMatrix.webidl',
     'WebSocket.webidl',
     'WheelEvent.webidl',
     'WidevineCDMManifest.webidl',
     'WindowOrWorkerGlobalScope.webidl',
     'WindowRoot.webidl',
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/WebExtensionPolicy.cpp
@@ -0,0 +1,235 @@
+/* -*-  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 "mozilla/ExtensionPolicyService.h"
+#include "mozilla/extensions/WebExtensionPolicy.h"
+
+#include "nsEscape.h"
+#include "nsISubstitutingProtocolHandler.h"
+#include "nsNetUtil.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla {
+namespace extensions {
+
+using namespace dom;
+
+static inline Result<Ok, nsresult>
+WrapNSResult(PRStatus aRv)
+{
+  if (aRv != PR_SUCCESS) {
+    return Err(NS_ERROR_FAILURE);
+  }
+  return Ok();
+}
+
+static inline Result<Ok, nsresult>
+WrapNSResult(nsresult aRv)
+{
+  if (NS_FAILED(aRv)) {
+    return Err(aRv);
+  }
+  return Ok();
+}
+
+#define NS_TRY(expr) MOZ_TRY(WrapNSResult(expr))
+
+static const char kProto[] = "moz-extension";
+
+static const char kBackgroundPageHTMLStart[] = "<!DOCTYPE html>\n\
+<html>\n\
+  <head><meta charset=\"utf-8\"></head>\n\
+  <body>";
+
+static const char kBackgroundPageHTMLScript[] = "\n\
+    <script type=\"text/javascript\" src=\"%s\"></script>";
+
+static const char kBackgroundPageHTMLEnd[] = "\n\
+  <body>\n\
+</html>";
+
+class EscapeHTML final : public nsAdoptingCString
+{
+public:
+  explicit EscapeHTML(const nsACString& str)
+    : nsAdoptingCString(nsEscapeHTML(str.BeginReading()))
+  {}
+};
+
+
+static nsISubstitutingProtocolHandler*
+Proto()
+{
+  static nsCOMPtr<nsISubstitutingProtocolHandler> sHandler;
+
+  if (MOZ_UNLIKELY(!sHandler)) {
+    nsCOMPtr<nsIIOService> ios = do_GetIOService();
+    MOZ_RELEASE_ASSERT(ios);
+
+    nsCOMPtr<nsIProtocolHandler> handler;
+    ios->GetProtocolHandler(kProto, getter_AddRefs(handler));
+
+    sHandler = do_QueryInterface(handler);
+    MOZ_RELEASE_ASSERT(sHandler);
+
+    ClearOnShutdown(&sHandler);
+  }
+
+  return sHandler;
+}
+
+
+/*****************************************************************************
+ * WebExtensionPolicy
+ *****************************************************************************/
+
+WebExtensionPolicy::WebExtensionPolicy(GlobalObject& aGlobal,
+                                       const WebExtensionInit& aInit,
+                                       ErrorResult& aRv)
+  : mId(NS_AtomizeMainThread(aInit.mId))
+  , mHostname(aInit.mMozExtensionHostname)
+  , mContentSecurityPolicy(aInit.mContentSecurityPolicy)
+  , mLocalizeCallback(aInit.mLocalizeCallback)
+  , mPermissions(new AtomSet(aInit.mPermissions))
+  , mHostPermissions(aInit.mAllowedOrigins)
+{
+  mWebAccessiblePaths.AppendElements(aInit.mWebAccessibleResources);
+
+  if (!aInit.mBackgroundScripts.IsNull()) {
+    mBackgroundScripts.SetValue().AppendElements(aInit.mBackgroundScripts.Value());
+  }
+
+  nsresult rv = NS_NewURI(getter_AddRefs(mBaseURI), aInit.mBaseURL);
+  if (NS_FAILED(rv)) {
+    aRv.Throw(rv);
+  }
+}
+
+already_AddRefed<WebExtensionPolicy>
+WebExtensionPolicy::Constructor(GlobalObject& aGlobal,
+                                const WebExtensionInit& aInit,
+                                ErrorResult& aRv)
+{
+  RefPtr<WebExtensionPolicy> policy = new WebExtensionPolicy(aGlobal, aInit, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+  return policy.forget();
+}
+
+
+void
+WebExtensionPolicy::SetActive(bool aActive, ErrorResult& aRv)
+{
+  if (aActive == mActive) {
+    return;
+  }
+
+  bool ok = aActive ? Enable() : Disable();
+
+  if (!ok) {
+    aRv.Throw(NS_ERROR_UNEXPECTED);
+  }
+}
+
+bool
+WebExtensionPolicy::Enable()
+{
+  MOZ_ASSERT(!mActive);
+
+  Unused << Proto()->SetSubstitution(MozExtensionHostname(), mBaseURI);
+
+  mActive = true;
+  return true;
+}
+
+bool
+WebExtensionPolicy::Disable()
+{
+  MOZ_ASSERT(mActive);
+
+  Unused << Proto()->SetSubstitution(MozExtensionHostname(), nullptr);
+
+  mActive = false;
+  return true;
+}
+
+void
+WebExtensionPolicy::GetURL(const nsAString& aPath,
+                           nsAString& aResult,
+                           ErrorResult& aRv) const
+{
+  auto result = GetURL(aPath);
+  if (result.isOk()) {
+    aResult = result.unwrap();
+  } else {
+    aRv.Throw(result.unwrapErr());
+  }
+}
+
+Result<nsString, nsresult>
+WebExtensionPolicy::GetURL(const nsAString& aPath) const
+{
+  nsPrintfCString spec("%s://%s/", kProto, mHostname.get());
+
+  nsCOMPtr<nsIURI> uri;
+  NS_TRY(NS_NewURI(getter_AddRefs(uri), spec));
+
+  NS_TRY(uri->Resolve(NS_ConvertUTF16toUTF8(aPath), spec));
+
+  return NS_ConvertUTF8toUTF16(spec);
+}
+
+nsCString
+WebExtensionPolicy::BackgroundPageHTML() const
+{
+  nsAutoCString result;
+
+  if (mBackgroundScripts.IsNull()) {
+    result.SetIsVoid(true);
+    return result;
+  }
+
+  result.AppendLiteral(kBackgroundPageHTMLStart);
+
+  for (auto& script : mBackgroundScripts.Value()) {
+    EscapeHTML escaped{NS_ConvertUTF16toUTF8(script)};
+
+    result.AppendPrintf(kBackgroundPageHTMLScript, escaped.get());
+  }
+
+  result.AppendLiteral(kBackgroundPageHTMLEnd);
+  return result;
+}
+
+void
+WebExtensionPolicy::Localize(const nsAString& aInput, nsString& aOutput) const
+{
+  mLocalizeCallback->Call(aInput, aOutput);
+}
+
+
+JSObject*
+WebExtensionPolicy::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto)
+{
+  return WebExtensionPolicyBinding::Wrap(aCx, this, aGivenProto);
+}
+
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(WebExtensionPolicy, mParent,
+                                      mLocalizeCallback,
+                                      mHostPermissions,
+                                      mWebAccessiblePaths)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebExtensionPolicy)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(WebExtensionPolicy)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(WebExtensionPolicy)
+
+} // namespace extensions
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/WebExtensionPolicy.h
@@ -0,0 +1,149 @@
+/* -*-  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/. */
+
+#ifndef mozilla_extensions_WebExtensionPolicy_h
+#define mozilla_extensions_WebExtensionPolicy_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/WebExtensionPolicyBinding.h"
+#include "mozilla/extensions/MatchPattern.h"
+
+#include "jspubtd.h"
+
+#include "mozilla/Result.h"
+#include "mozilla/WeakPtr.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+namespace extensions {
+
+using dom::WebExtensionInit;
+using dom::WebExtensionLocalizeCallback;
+
+class WebExtensionPolicy final : public nsISupports
+                               , public nsWrapperCache
+                               , public SupportsWeakPtr<WebExtensionPolicy>
+{
+public:
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WebExtensionPolicy)
+  MOZ_DECLARE_WEAKREFERENCE_TYPENAME(WebExtensionPolicy)
+
+  static already_AddRefed<WebExtensionPolicy>
+  Constructor(dom::GlobalObject& aGlobal, const WebExtensionInit& aInit, ErrorResult& aRv);
+
+  nsIAtom* Id() const { return mId; }
+  void GetId(nsAString& aId) const { aId = nsDependentAtomString(mId); };
+
+  const nsCString& MozExtensionHostname() const { return mHostname; }
+  void GetMozExtensionHostname(nsACString& aHostname) const
+  {
+    aHostname = MozExtensionHostname();
+  }
+
+  void GetBaseURL(nsACString& aBaseURL) const
+  {
+    MOZ_ALWAYS_SUCCEEDS(mBaseURI->GetSpec(aBaseURL));
+  }
+
+  void GetURL(const nsAString& aPath, nsAString& aURL, ErrorResult& aRv) const;
+
+  Result<nsString, nsresult> GetURL(const nsAString& aPath) const;
+
+  bool CanAccessURI(nsIURI* aURI, bool aExplicit = false) const
+  {
+    return mHostPermissions && mHostPermissions->Matches(aURI, aExplicit);
+  }
+
+  bool IsPathWebAccessible(const nsAString& aPath) const
+  {
+    return mWebAccessiblePaths.Matches(aPath);
+  }
+
+  bool HasPermission(const nsIAtom* aPermission) const
+  {
+    return mPermissions->Contains(aPermission);
+  }
+  bool HasPermission(const nsAString& aPermission) const
+  {
+    return mPermissions->Contains(aPermission);
+  }
+
+  nsCString BackgroundPageHTML() const;
+
+  void Localize(const nsAString& aInput, nsString& aResult) const;
+
+  const nsString& ContentSecurityPolicy() const
+  {
+    return mContentSecurityPolicy;
+  }
+  void GetContentSecurityPolicy(nsAString& aCSP) const
+  {
+    aCSP = mContentSecurityPolicy;
+  }
+
+
+  already_AddRefed<MatchPatternSet> AllowedOrigins()
+  {
+    return do_AddRef(mHostPermissions);
+  }
+  void SetAllowedOrigins(MatchPatternSet& aAllowedOrigins)
+  {
+    mHostPermissions = &aAllowedOrigins;
+  }
+
+  void GetPermissions(nsTArray<nsString>& aResult) const
+  {
+    mPermissions->Get(aResult);
+  }
+  void SetPermissions(const nsTArray<nsString>& aPermissions)
+  {
+    mPermissions = new AtomSet(aPermissions);
+  }
+
+
+  bool Active() const { return mActive; }
+  void SetActive(bool aActive, ErrorResult& aRv);
+
+
+  nsISupports* GetParentObject() const { return mParent; }
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override;
+
+protected:
+  virtual ~WebExtensionPolicy() = default;
+
+private:
+  WebExtensionPolicy(dom::GlobalObject& aGlobal, const WebExtensionInit& aInit, ErrorResult& aRv);
+
+  bool Enable();
+  bool Disable();
+
+  nsCOMPtr<nsISupports> mParent;
+
+  nsCOMPtr<nsIAtom> mId;
+  nsCString mHostname;
+  nsCOMPtr<nsIURI> mBaseURI;
+
+  nsString mContentSecurityPolicy;
+
+  bool mActive = false;
+
+  RefPtr<WebExtensionLocalizeCallback> mLocalizeCallback;
+
+  RefPtr<AtomSet> mPermissions;
+  RefPtr<MatchPatternSet> mHostPermissions;
+  MatchGlobSet mWebAccessiblePaths;
+
+  Nullable<nsTArray<nsString>> mBackgroundScripts;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_WebExtensionPolicy_h
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -43,20 +43,22 @@ TESTING_JS_MODULES += [
 DIRS += [
     'schemas',
     'webrequest',
 ]
 
 EXPORTS.mozilla.extensions = [
     'MatchGlob.h',
     'MatchPattern.h',
+    'WebExtensionPolicy.h',
 ]
 
 UNIFIED_SOURCES += [
     'MatchPattern.cpp',
+    'WebExtensionPolicy.cpp',
 ]
 
 FINAL_LIBRARY = 'xul'
 
 
 JAR_MANIFESTS += ['jar.mn']
 
 BROWSER_CHROME_MANIFESTS += [
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js
@@ -0,0 +1,78 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const {newURI} = Services.io;
+
+add_task(async function test_WebExtensinonPolicy() {
+  const id = "foo@bar.baz";
+  const uuid = "ca9d3f23-125c-4b24-abfc-1ca2692b0610";
+
+  const baseURL = "file:///foo/";
+  const mozExtURL = `moz-extension://${uuid}/`;
+  const mozExtURI = newURI(mozExtURL);
+
+  let policy = new WebExtensionPolicy({
+    id,
+    mozExtensionHostname: uuid,
+    baseURL,
+
+    localizeCallback(str) {
+      return `<${str}>`;
+    },
+
+    allowedOrigins: new MatchPatternSet(["http://foo.bar/", "*://*.baz/"], {ignorePath: true}),
+    permissions: ["<all_urls>"],
+    webAccessibleResources: ["/foo/*", "/bar.baz"].map(glob => new MatchGlob(glob)),
+  });
+
+  equal(policy.active, false, "Active attribute should initially be false");
+
+  // GetURL
+
+  equal(policy.getURL(), mozExtURL, "getURL() should return the correct root URL");
+  equal(policy.getURL("path/foo.html"), `${mozExtURL}path/foo.html`, "getURL(path) should return the correct URL");
+
+
+  // Permissions
+
+  deepEqual(policy.permissions, ["<all_urls>"], "Initial permissions should be correct");
+
+  ok(policy.hasPermission("<all_urls>"), "hasPermission should match existing permission");
+  ok(!policy.hasPermission("history"), "hasPermission should not match nonexistent permission");
+
+  Assert.throws(() => { policy.permissions[0] = "foo"; },
+                TypeError,
+                "Permissions array should be frozen");
+
+  policy.permissions = ["history"];
+  deepEqual(policy.permissions, ["history"], "Permissions should be updateable as a set");
+
+  ok(policy.hasPermission("history"), "hasPermission should match existing permission");
+  ok(!policy.hasPermission("<all_urls>"), "hasPermission should not match nonexistent permission");
+
+
+  // Origins
+
+  ok(policy.canAccessURI(newURI("http://foo.bar/quux")), "Should be able to access whitelisted URI");
+  ok(policy.canAccessURI(newURI("https://x.baz/foo")), "Should be able to access whitelisted URI");
+
+  ok(!policy.canAccessURI(newURI("https://foo.bar/quux")), "Should not be able to access non-whitelisted URI");
+
+  policy.allowedOrigins = new MatchPatternSet(["https://foo.bar/"], {ignorePath: true});
+
+  ok(policy.canAccessURI(newURI("https://foo.bar/quux")), "Should be able to access updated whitelisted URI");
+  ok(!policy.canAccessURI(newURI("https://x.baz/foo")), "Should not be able to access removed whitelisted URI");
+
+
+  // Web-accessible resources
+
+  ok(policy.isPathWebAccessible("/foo/bar"), "Web-accessible glob should be web-accessible");
+  ok(policy.isPathWebAccessible("/bar.baz"), "Web-accessible path should be web-accessible");
+  ok(!policy.isPathWebAccessible("/bar.baz/quux"), "Non-web-accessible path should not be web-accessible");
+
+
+  // Localization
+
+  equal(policy.localize("foo"), "<foo>", "Localization callback should work as expected");
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -5,16 +5,17 @@ skip-if = appname == "thunderbird"
 dupe-manifest =
 support-files =
   data/**
   head_sync.js
   xpcshell-content.ini
 tags = webextensions
 
 [test_MatchPattern.js]
+[test_WebExtensionPolicy.js]
 
 [test_csp_custom_policies.js]
 [test_csp_validator.js]
 [test_ext_alarms.js]
 [test_ext_alarms_does_not_fire.js]
 [test_ext_alarms_periodic.js]
 [test_ext_alarms_replaces.js]
 [test_ext_api_permissions.js]