Bug 1368102: Part 2 - Add WebExtensionContentScript bindings. r=billm,mixedpuppy
authorKris Maglione <maglione.k@gmail.com>
Sat, 03 Jun 2017 22:03:19 -0700
changeset 410374 056e3d934eb26523b35fcdd4b1cdadca2c8bb190
parent 410373 863506a00d8362e8fe180001d0d4183c1b1f9d3c
child 410375 b9439245aa6bc591c4e138ea2f6978d7e3bab831
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, mixedpuppy
bugs1368102
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 1368102: Part 2 - Add WebExtensionContentScript bindings. r=billm,mixedpuppy Bill, can you please review the binding code? Shane and zombie, can you please review the content script matching? MozReview-Commit-ID: IJB5s0a7r7S
dom/base/nsGkAtomList.h
dom/bindings/Bindings.conf
dom/webidl/WebExtensionContentScript.webidl
dom/webidl/WebExtensionPolicy.webidl
dom/webidl/moz.build
toolkit/components/extensions/.eslintrc.js
toolkit/components/extensions/ExtensionPolicyService.cpp
toolkit/components/extensions/MatchPattern.cpp
toolkit/components/extensions/WebExtensionContentScript.h
toolkit/components/extensions/WebExtensionPolicy.cpp
toolkit/components/extensions/WebExtensionPolicy.h
toolkit/components/extensions/moz.build
toolkit/components/extensions/test/xpcshell/data/file_iframe.html
toolkit/components/extensions/test/xpcshell/data/file_toplevel.html
toolkit/components/extensions/test/xpcshell/test_WebExtensionContentScript.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
--- a/dom/base/nsGkAtomList.h
+++ b/dom/base/nsGkAtomList.h
@@ -2006,16 +2006,17 @@ GK_ATOM(ondevicelight, "ondevicelight")
 // MediaDevices device change event
 GK_ATOM(ondevicechange, "ondevicechange")
 
 // HTML element attributes that only exposed to XBL and chrome content
 GK_ATOM(mozinputrangeignorepreventdefault, "mozinputrangeignorepreventdefault")
 
 // WebExtensions
 GK_ATOM(moz_extension, "moz-extension")
+GK_ATOM(all_urlsPermission, "<all_urls>")
 GK_ATOM(http, "http")
 GK_ATOM(https, "https")
 
 //---------------------------------------------------------------------------
 // Special atoms
 //---------------------------------------------------------------------------
 
 // Node types
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -1096,16 +1096,20 @@ DOMInterfaces = {
 'VTTRegion': {
   'nativeType': 'mozilla::dom::TextTrackRegion',
 },
 
 'WebAuthentication': {
     'implicitJSContext': 'makeCredential',
 },
 
+'WebExtensionContentScript': {
+    'nativeType': 'mozilla::extensions::WebExtensionContentScript',
+},
+
 'WebExtensionPolicy': {
     'nativeType': 'mozilla::extensions::WebExtensionPolicy',
 },
 
 'WindowClient': {
     'nativeType': 'mozilla::dom::workers::ServiceWorkerWindowClient',
     'headerFile': 'mozilla/dom/workers/bindings/ServiceWorkerWindowClient.h',
 },
@@ -1708,16 +1712,18 @@ def addExternalIface(iface, nativeType=N
 addExternalIface('ApplicationCache', nativeType='nsIDOMOfflineResourceList')
 addExternalIface('Cookie', nativeType='nsICookie2',
                  headerFile='nsICookie2.h', notflattened=True)
 addExternalIface('Counter')
 addExternalIface('RTCDataChannel', nativeType='nsIDOMDataChannel')
 addExternalIface('HitRegionOptions', nativeType='nsISupports')
 addExternalIface('imgINotificationObserver', nativeType='imgINotificationObserver')
 addExternalIface('imgIRequest', nativeType='imgIRequest', notflattened=True)
+addExternalIface('LoadInfo', nativeType='nsILoadInfo',
+                 headerFile='nsILoadInfo.h', notflattened=True)
 addExternalIface('MenuBuilder', nativeType='nsIMenuBuilder', notflattened=True)
 addExternalIface('MozControllers', nativeType='nsIControllers')
 addExternalIface('MozFrameLoader', nativeType='nsIFrameLoader', notflattened=True)
 addExternalIface('MozObserver', nativeType='nsIObserver', notflattened=True)
 addExternalIface('MozRDFCompositeDataSource', nativeType='nsIRDFCompositeDataSource',
                  notflattened=True)
 addExternalIface('MozRDFResource', nativeType='nsIRDFResource', notflattened=True)
 addExternalIface('MozTreeView', nativeType='nsITreeView',
new file mode 100644
--- /dev/null
+++ b/dom/webidl/WebExtensionContentScript.webidl
@@ -0,0 +1,161 @@
+/* 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 LoadInfo;
+interface URI;
+interface WindowProxy;
+
+/**
+ * Describes the earliest point in the load cycle at which a script should
+ * run.
+ */
+enum ContentScriptRunAt {
+  /**
+   * The point in the load cycle just after the document element has been
+   * inserted, before any page scripts have been allowed to run.
+   */
+  "document_start",
+  /**
+   * The point after which the page DOM has fully loaded, but before all page
+   * resources have necessarily been loaded. Corresponds approximately to the
+   * DOMContentLoaded event.
+   */
+  "document_end",
+  /**
+   * The first point after the page and all of its resources has fully loaded
+   * when the event loop is idle, and can run scripts without delaying a paint
+   * event.
+   */
+  "document_idle",
+};
+
+[Constructor(WebExtensionPolicy extension, WebExtensionContentScriptInit options), ChromeOnly, Exposed=System]
+interface WebExtensionContentScript {
+  /**
+   * Returns true if the script's match and exclude patterns match the given
+   * URI, without reference to attributes such as `allFrames`.
+   */
+  boolean matchesURI(URI uri);
+
+  /**
+   * Returns true if the script matches the given URI and LoadInfo objects.
+   * This should be used to determine whether to begin pre-loading a content
+   * script based on network events.
+   */
+  boolean matchesLoadInfo(URI uri, LoadInfo loadInfo);
+
+  /**
+   * Returns true if the script matches the given window. This should be used
+   * to determine whether to run a script in a window at load time.
+   */
+  boolean matchesWindow(WindowProxy window);
+
+  /**
+   * The policy object for the extension that this script belongs to.
+   */
+  [Constant]
+  readonly attribute WebExtensionPolicy extension;
+
+  /**
+   * If true, this script runs in all frames. If false, it only runs in
+   * top-level frames.
+   */
+  [Constant]
+  readonly attribute boolean allFrames;
+
+  /**
+   * If true, this (misleadingly-named, but inherited from Chrome) attribute
+   * causes the script to run in frames with URLs which inherit a principal
+   * that matches one of the match patterns, such as about:blank or
+   * about:srcdoc. If false, the script only runs in frames with an explicit
+   * matching URL.
+   */
+  [Constant]
+  readonly attribute boolean matchAboutBlank;
+
+  /**
+   * The earliest point in the load cycle at which this script should run. For
+   * static content scripts, in extensions which were present at browser
+   * startup, the browser makes every effort to make sure that the script runs
+   * no later than this point in the load cycle. For dynamic content scripts,
+   * and scripts from extensions installed during this session, the scripts
+   * may run at a later point.
+   */
+  [Constant]
+  readonly attribute ContentScriptRunAt runAt;
+
+  /**
+   * The outer window ID of the frame in which to run the script, or 0 if it
+   * should run in the top-level frame. Should only be used for
+   * dynamically-injected scripts.
+   */
+  [Constant]
+  readonly attribute unsigned long long? frameID;
+
+  /**
+   * The set of match patterns for URIs of pages in which this script should
+   * run. This attribute is mandatory, and is a prerequisite for all other
+   * match patterns.
+   */
+  [Constant]
+  readonly attribute MatchPatternSet matches;
+
+  /**
+   * A set of match patterns for URLs in which this script should not run,
+   * even if they match other include patterns or globs.
+   */
+  [Constant]
+  readonly attribute MatchPatternSet? excludeMatches;
+
+  /**
+   * A set of glob matchers for URLs in which this script should run. If this
+   * list is present, the script will only run in URLs which match the
+   * `matches` pattern as well as one of these globs.
+   */
+  [Cached, Constant, Frozen]
+  readonly attribute sequence<MatchGlob>? includeGlobs;
+
+  /**
+   * A set of glob matchers for URLs in which this script should not run, even
+   * if they match other include patterns or globs.
+   */
+  [Cached, Constant, Frozen]
+  readonly attribute sequence<MatchGlob>? excludeGlobs;
+
+  /**
+   * A set of paths, relative to the extension root, of CSS sheets to inject
+   * into matching pages.
+   */
+  [Cached, Constant, Frozen]
+  readonly attribute sequence<DOMString> cssPaths;
+
+  /**
+   * A set of paths, relative to the extension root, of JavaScript scripts to
+   * execute in matching pages.
+   */
+  [Cached, Constant, Frozen]
+  readonly attribute sequence<DOMString> jsPaths;
+};
+
+dictionary WebExtensionContentScriptInit {
+  boolean allFrames = false;
+
+  boolean matchAboutBlank = false;
+
+  ContentScriptRunAt runAt = "document_idle";
+
+  unsigned long long? frameID = null;
+
+  required MatchPatternSet matches;
+
+  MatchPatternSet? excludeMatches = null;
+
+  sequence<MatchGlob>? includeGlobs = null;
+
+  sequence<MatchGlob>? excludeGlobs = null;
+
+  sequence<DOMString> cssPaths = [];
+
+  sequence<DOMString> jsPaths = [];
+};
--- a/dom/webidl/WebExtensionPolicy.webidl
+++ b/dom/webidl/WebExtensionPolicy.webidl
@@ -54,16 +54,22 @@ interface WebExtensionPolicy {
    * 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;
 
   /**
+   * The set of content scripts active for this extension.
+   */
+  [Cached, Constant, Frozen]
+  readonly attribute sequence<WebExtensionContentScript> contentScripts;
+
+  /**
    * 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.
    */
@@ -136,12 +142,14 @@ dictionary WebExtensionInit {
   required WebExtensionLocalizeCallback localizeCallback;
 
   required MatchPatternSet allowedOrigins;
 
   sequence<DOMString> permissions = [];
 
   sequence<MatchGlob> webAccessibleResources = [];
 
+  sequence<WebExtensionContentScriptInit> contentScripts = [];
+
   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',
+    'WebExtensionContentScript.webidl',
     'WebExtensionPolicy.webidl',
     'WebGL2RenderingContext.webidl',
     'WebGLRenderingContext.webidl',
     'WebKitCSSMatrix.webidl',
     'WebSocket.webidl',
     'WheelEvent.webidl',
     'WidevineCDMManifest.webidl',
     'WindowOrWorkerGlobalScope.webidl',
--- a/toolkit/components/extensions/.eslintrc.js
+++ b/toolkit/components/extensions/.eslintrc.js
@@ -8,17 +8,19 @@ module.exports = {
     "Cr": true,
     "Cu": true,
     "TextDecoder": false,
     "TextEncoder": false,
 
     "MatchGlob": false,
     "MatchPattern": true,
     "MatchPatternSet": false,
+    "WebExtensionContentScript": false,
     "WebExtensionPolicy": false,
+
     // Specific to WebExtensions:
     "AppConstants": true,
     "Extension": true,
     "ExtensionAPI": true,
     "ExtensionManagement": true,
     "ExtensionUtils": true,
     "extensions": true,
     "getContainerForCookieStoreId": true,
--- a/toolkit/components/extensions/ExtensionPolicyService.cpp
+++ b/toolkit/components/extensions/ExtensionPolicyService.cpp
@@ -1,14 +1,15 @@
 /* -*-  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/WebExtensionContentScript.h"
 #include "mozilla/extensions/WebExtensionPolicy.h"
 
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/Preferences.h"
 #include "nsEscape.h"
 #include "nsGkAtoms.h"
 
 namespace mozilla {
--- a/toolkit/components/extensions/MatchPattern.cpp
+++ b/toolkit/components/extensions/MatchPattern.cpp
@@ -180,20 +180,26 @@ URLInfo::URINoRef() const
   }
   return mURINoRef;
 }
 
 bool
 URLInfo::InheritsPrincipal() const
 {
   if (!mInheritsPrincipal.isSome()) {
-    bool inherits = false;
-    nsresult rv = NS_URIChainHasFlags(mURI, nsIProtocolHandler::URI_INHERITS_SECURITY_CONTEXT,
-                                      &inherits);
-    Unused << NS_WARN_IF(NS_FAILED(rv));
+    // For our purposes, about:blank and about:srcdoc are treated as URIs that
+    // inherit principals.
+    bool inherits = Spec().EqualsLiteral("about:blank") || Spec().EqualsLiteral("about:srcdoc");
+
+    if (!inherits) {
+      nsresult rv = NS_URIChainHasFlags(mURI, nsIProtocolHandler::URI_INHERITS_SECURITY_CONTEXT,
+                                        &inherits);
+      Unused << NS_WARN_IF(NS_FAILED(rv));
+    }
+
     mInheritsPrincipal.emplace(inherits);
   }
   return mInheritsPrincipal.ref();
 }
 
 
 /*****************************************************************************
  * CookieInfo
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/WebExtensionContentScript.h
@@ -0,0 +1,178 @@
+/* -*-  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_WebExtensionContentScript_h
+#define mozilla_extensions_WebExtensionContentScript_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/WebExtensionContentScriptBinding.h"
+
+#include "jspubtd.h"
+
+#include "mozilla/Maybe.h"
+#include "mozilla/Variant.h"
+#include "mozilla/extensions/MatchGlob.h"
+#include "mozilla/extensions/MatchPattern.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+class nsILoadInfo;
+class nsPIDOMWindowOuter;
+
+namespace mozilla {
+namespace extensions {
+
+using dom::Nullable;
+using ContentScriptInit = dom::WebExtensionContentScriptInit;
+
+class WebExtensionPolicy;
+
+class MOZ_STACK_CLASS DocInfo final
+{
+public:
+  DocInfo(const URLInfo& aURL, nsILoadInfo* aLoadInfo);
+
+  MOZ_IMPLICIT DocInfo(nsPIDOMWindowOuter* aWindow);
+
+  const URLInfo& URL() const { return mURL; }
+
+  nsIPrincipal* Principal() const;
+
+  const URLInfo& PrincipalURL() const;
+
+  bool IsTopLevel() const;
+
+  uint64_t FrameID() const;
+
+private:
+  void SetURL(const URLInfo& aURL);
+
+  const URLInfo mURL;
+  mutable Maybe<const URLInfo> mPrincipalURL;
+
+  mutable Maybe<bool> mIsTopLevel;
+  mutable Maybe<nsCOMPtr<nsIPrincipal>> mPrincipal;
+  mutable Maybe<uint64_t> mFrameID;
+
+  using Window = nsPIDOMWindowOuter*;
+  using LoadInfo = nsILoadInfo*;
+
+  const Variant<LoadInfo, Window> mObj;
+};
+
+
+class WebExtensionContentScript final : public nsISupports
+                                      , public nsWrapperCache
+{
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WebExtensionContentScript)
+
+
+  using MatchGlobArray = nsTArray<RefPtr<MatchGlob>>;
+  using RunAtEnum = dom::ContentScriptRunAt;
+
+  static already_AddRefed<WebExtensionContentScript>
+  Constructor(dom::GlobalObject& aGlobal,
+              WebExtensionPolicy& aExtension,
+              const ContentScriptInit& aInit,
+              ErrorResult& aRv);
+
+
+  bool Matches(const DocInfo& aDoc) const;
+  bool MatchesURI(const URLInfo& aURL) const;
+
+  bool MatchesLoadInfo(const URLInfo& aURL, nsILoadInfo* aLoadInfo) const
+  {
+    return Matches({aURL, aLoadInfo});
+  }
+  bool MatchesWindow(nsPIDOMWindowOuter* aWindow) const
+  {
+    return Matches(aWindow);
+  }
+
+
+  WebExtensionPolicy* Extension() { return mExtension; }
+  const WebExtensionPolicy* Extension() const { return mExtension; }
+
+  bool AllFrames() const { return mAllFrames; }
+  bool MatchAboutBlank() const { return mMatchAboutBlank; }
+  RunAtEnum RunAt() const { return mRunAt; }
+
+  Nullable<uint64_t> GetFrameID() const { return mFrameID; }
+
+  MatchPatternSet* Matches() { return mMatches; }
+  const MatchPatternSet* GetMatches() const { return mMatches; }
+
+  MatchPatternSet* GetExcludeMatches() { return mExcludeMatches; }
+  const MatchPatternSet* GetExcludeMatches() const { return mExcludeMatches; }
+
+  void GetIncludeGlobs(Nullable<MatchGlobArray>& aGlobs)
+  {
+    ToNullable(mExcludeGlobs, aGlobs);
+  }
+  void GetExcludeGlobs(Nullable<MatchGlobArray>& aGlobs)
+  {
+    ToNullable(mExcludeGlobs, aGlobs);
+  }
+
+  void GetCssPaths(nsTArray<nsString>& aPaths) const
+  {
+    aPaths.AppendElements(mCssPaths);
+  }
+  void GetJsPaths(nsTArray<nsString>& aPaths) const
+  {
+    aPaths.AppendElements(mJsPaths);
+  }
+
+
+  WebExtensionPolicy* GetParentObject() const { return mExtension; }
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override;
+
+protected:
+  friend class WebExtensionPolicy;
+
+  virtual ~WebExtensionContentScript() = default;
+
+  WebExtensionContentScript(WebExtensionPolicy& aExtension,
+                            const ContentScriptInit& aInit,
+                            ErrorResult& aRv);
+
+private:
+  RefPtr<WebExtensionPolicy> mExtension;
+
+  RefPtr<MatchPatternSet> mMatches;
+  RefPtr<MatchPatternSet> mExcludeMatches;
+
+  Nullable<MatchGlobSet> mIncludeGlobs;
+  Nullable<MatchGlobSet> mExcludeGlobs;
+
+  nsTArray<nsString> mCssPaths;
+  nsTArray<nsString> mJsPaths;
+
+  RunAtEnum mRunAt;
+
+  bool mAllFrames;
+  Nullable<uint64_t> mFrameID;
+  bool mMatchAboutBlank;
+
+  template <typename T, typename U>
+  void
+  ToNullable(const Nullable<T>& aInput, Nullable<U>& aOutput)
+  {
+    if (aInput.IsNull()) {
+      aOutput.SetNull();
+    } else {
+      aOutput.SetValue(aInput.Value());
+    }
+  }
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_WebExtensionContentScript_h
--- a/toolkit/components/extensions/WebExtensionPolicy.cpp
+++ b/toolkit/components/extensions/WebExtensionPolicy.cpp
@@ -1,16 +1,18 @@
 /* -*-  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/WebExtensionContentScript.h"
 #include "mozilla/extensions/WebExtensionPolicy.h"
 
+#include "mozilla/AddonManagerWebAPI.h"
 #include "nsEscape.h"
 #include "nsISubstitutingProtocolHandler.h"
 #include "nsNetUtil.h"
 #include "nsPrintfCString.h"
 
 namespace mozilla {
 namespace extensions {
 
@@ -106,16 +108,26 @@ WebExtensionPolicy::WebExtensionPolicy(G
   if (!aInit.mBackgroundScripts.IsNull()) {
     mBackgroundScripts.SetValue().AppendElements(aInit.mBackgroundScripts.Value());
   }
 
   if (mContentSecurityPolicy.IsVoid()) {
     EPS().DefaultCSP(mContentSecurityPolicy);
   }
 
+  mContentScripts.SetCapacity(aInit.mContentScripts.Length());
+  for (const auto& scriptInit : aInit.mContentScripts) {
+    RefPtr<WebExtensionContentScript> contentScript =
+      new WebExtensionContentScript(*this, scriptInit, aRv);
+    if (aRv.Failed()) {
+      return;
+    }
+    mContentScripts.AppendElement(Move(contentScript));
+  }
+
   nsresult rv = NS_NewURI(getter_AddRefs(mBaseURI), aInit.mBaseURL);
   if (NS_FAILED(rv)) {
     aRv.Throw(rv);
   }
 }
 
 already_AddRefed<WebExtensionPolicy>
 WebExtensionPolicy::Constructor(GlobalObject& aGlobal,
@@ -257,24 +269,228 @@ WebExtensionPolicy::Localize(const nsASt
 
 
 JSObject*
 WebExtensionPolicy::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto)
 {
   return WebExtensionPolicyBinding::Wrap(aCx, this, aGivenProto);
 }
 
+void
+WebExtensionPolicy::GetContentScripts(nsTArray<RefPtr<WebExtensionContentScript>>& aScripts) const
+{
+  aScripts.AppendElements(mContentScripts);
+}
+
 
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(WebExtensionPolicy, mParent,
                                       mLocalizeCallback,
                                       mHostPermissions,
-                                      mWebAccessiblePaths)
+                                      mWebAccessiblePaths,
+                                      mContentScripts)
 
 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)
 
+
+/*****************************************************************************
+ * WebExtensionContentScript
+ *****************************************************************************/
+
+/* static */ already_AddRefed<WebExtensionContentScript>
+WebExtensionContentScript::Constructor(GlobalObject& aGlobal,
+                                       WebExtensionPolicy& aExtension,
+                                       const ContentScriptInit& aInit,
+                                       ErrorResult& aRv)
+{
+  RefPtr<WebExtensionContentScript> script = new WebExtensionContentScript(aExtension, aInit, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+  return script.forget();
+}
+
+WebExtensionContentScript::WebExtensionContentScript(WebExtensionPolicy& aExtension,
+                                                     const ContentScriptInit& aInit,
+                                                     ErrorResult& aRv)
+  : mExtension(&aExtension)
+  , mMatches(aInit.mMatches)
+  , mExcludeMatches(aInit.mExcludeMatches)
+  , mCssPaths(aInit.mCssPaths)
+  , mJsPaths(aInit.mJsPaths)
+  , mRunAt(aInit.mRunAt)
+  , mAllFrames(aInit.mAllFrames)
+  , mFrameID(aInit.mFrameID)
+  , mMatchAboutBlank(aInit.mMatchAboutBlank)
+{
+  if (!aInit.mIncludeGlobs.IsNull()) {
+    mIncludeGlobs.SetValue().AppendElements(aInit.mIncludeGlobs.Value());
+  }
+
+  if (!aInit.mExcludeGlobs.IsNull()) {
+    mExcludeGlobs.SetValue().AppendElements(aInit.mExcludeGlobs.Value());
+  }
+}
+
+
+bool
+WebExtensionContentScript::Matches(const DocInfo& aDoc) const
+{
+  if (!mFrameID.IsNull()) {
+    if (aDoc.FrameID() != mFrameID.Value()) {
+      return false;
+    }
+  } else {
+    if (!mAllFrames && !aDoc.IsTopLevel()) {
+      return false;
+    }
+  }
+
+  if (!mMatchAboutBlank && aDoc.URL().InheritsPrincipal()) {
+    return false;
+  }
+
+  if (!MatchesURI(aDoc.PrincipalURL())) {
+    return false;
+  }
+
+  return true;
+}
+
+bool
+WebExtensionContentScript::MatchesURI(const URLInfo& aURL) const
+{
+  if (!mMatches->Matches(aURL)) {
+    return false;
+  }
+
+  if (mExcludeMatches && mExcludeMatches->Matches(aURL)) {
+    return false;
+  }
+
+  if (!mIncludeGlobs.IsNull() && !mIncludeGlobs.Value().Matches(aURL.Spec())) {
+    return false;
+  }
+
+  if (!mExcludeGlobs.IsNull() && mExcludeGlobs.Value().Matches(aURL.Spec())) {
+    return false;
+  }
+
+  if (AddonManagerWebAPI::IsValidSite(aURL.URI())) {
+    return false;
+  }
+
+  return true;
+}
+
+
+JSObject*
+WebExtensionContentScript::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto)
+{
+  return WebExtensionContentScriptBinding::Wrap(aCx, this, aGivenProto);
+}
+
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(WebExtensionContentScript,
+                                      mMatches, mExcludeMatches,
+                                      mIncludeGlobs, mExcludeGlobs,
+                                      mExtension)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebExtensionContentScript)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(WebExtensionContentScript)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(WebExtensionContentScript)
+
+
+/*****************************************************************************
+ * DocInfo
+ *****************************************************************************/
+
+DocInfo::DocInfo(const URLInfo& aURL, nsILoadInfo* aLoadInfo)
+  : mURL(aURL)
+  , mObj(AsVariant(aLoadInfo))
+{}
+
+DocInfo::DocInfo(nsPIDOMWindowOuter* aWindow)
+  : mURL(aWindow->GetDocumentURI())
+  , mObj(AsVariant(aWindow))
+{}
+
+bool
+DocInfo::IsTopLevel() const
+{
+  if (mIsTopLevel.isNothing()) {
+    struct Matcher
+    {
+      bool match(Window aWin) { return aWin->IsTopLevelWindow(); }
+      bool match(LoadInfo aLoadInfo) { return aLoadInfo->GetIsTopLevelLoad(); }
+    };
+    mIsTopLevel.emplace(mObj.match(Matcher()));
+  }
+  return mIsTopLevel.ref();
+}
+
+uint64_t
+DocInfo::FrameID() const
+{
+  if (mFrameID.isNothing()) {
+    if (IsTopLevel()) {
+      mFrameID.emplace(0);
+    } else {
+      struct Matcher
+      {
+        uint64_t match(Window aWin) { return aWin->GetCurrentInnerWindow()->WindowID(); }
+        uint64_t match(LoadInfo aLoadInfo) { return aLoadInfo->GetInnerWindowID(); }
+      };
+      mFrameID.emplace(mObj.match(Matcher()));
+    }
+  }
+  return mFrameID.ref();
+}
+
+nsIPrincipal*
+DocInfo::Principal() const
+{
+  if (mPrincipal.isNothing()) {
+    struct Matcher
+    {
+      nsIPrincipal* match(Window aWin)
+      {
+        nsCOMPtr<nsIDocument> doc = aWin->GetDoc();
+        return doc->NodePrincipal();
+      }
+      nsIPrincipal* match(LoadInfo aLoadInfo) { return aLoadInfo->PrincipalToInherit(); }
+    };
+    mPrincipal.emplace(mObj.match(Matcher()));
+  }
+  return mPrincipal.ref();
+}
+
+const URLInfo&
+DocInfo::PrincipalURL() const
+{
+  if (!URL().InheritsPrincipal()) {
+    return URL();
+  }
+
+  if (mPrincipalURL.isNothing()) {
+    nsIPrincipal* prin = Principal();
+    nsCOMPtr<nsIURI> uri;
+    if (prin && NS_SUCCEEDED(prin->GetURI(getter_AddRefs(uri)))) {
+      mPrincipalURL.emplace(uri);
+    } else {
+      mPrincipalURL.emplace(URL());
+    }
+  }
+
+  return mPrincipalURL.ref();
+}
+
 } // namespace extensions
 } // namespace mozilla
--- a/toolkit/components/extensions/WebExtensionPolicy.h
+++ b/toolkit/components/extensions/WebExtensionPolicy.h
@@ -20,25 +20,29 @@
 #include "nsWrapperCache.h"
 
 namespace mozilla {
 namespace extensions {
 
 using dom::WebExtensionInit;
 using dom::WebExtensionLocalizeCallback;
 
+class WebExtensionContentScript;
+
 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)
 
+  using ScriptArray = nsTArray<RefPtr<WebExtensionContentScript>>;
+
   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
@@ -101,16 +105,19 @@ public:
   {
     mPermissions->Get(aResult);
   }
   void SetPermissions(const nsTArray<nsString>& aPermissions)
   {
     mPermissions = new AtomSet(aPermissions);
   }
 
+  void GetContentScripts(ScriptArray& aScripts) const;
+  const ScriptArray& ContentScripts() const { return mContentScripts; }
+
 
   bool Active() const { return mActive; }
   void SetActive(bool aActive, ErrorResult& aRv);
 
 
   static void
   GetActiveExtensions(dom::GlobalObject& aGlobal, nsTArray<RefPtr<WebExtensionPolicy>>& aResults);
 
@@ -149,14 +156,16 @@ private:
 
   RefPtr<WebExtensionLocalizeCallback> mLocalizeCallback;
 
   RefPtr<AtomSet> mPermissions;
   RefPtr<MatchPatternSet> mHostPermissions;
   MatchGlobSet mWebAccessiblePaths;
 
   Nullable<nsTArray<nsString>> mBackgroundScripts;
+
+  nsTArray<RefPtr<WebExtensionContentScript>> mContentScripts;
 };
 
 } // namespace extensions
 } // namespace mozilla
 
 #endif // mozilla_extensions_WebExtensionPolicy_h
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -47,16 +47,17 @@ DIRS += [
 
 EXPORTS.mozilla = [
     'ExtensionPolicyService.h',
 ]
 
 EXPORTS.mozilla.extensions = [
     'MatchGlob.h',
     'MatchPattern.h',
+    'WebExtensionContentScript.h',
     'WebExtensionPolicy.h',
 ]
 
 UNIFIED_SOURCES += [
     'ExtensionPolicyService.cpp',
     'MatchPattern.cpp',
     'WebExtensionPolicy.cpp',
 ]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/data/file_iframe.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>Iframe document</title>
+</head>
+<body>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/data/file_toplevel.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>Top-level frame document</title>
+</head>
+<body>
+  <iframe src="file_iframe.html"></iframe>
+  <iframe src="about:blank"></iframe>
+  <iframe srcdoc="Iframe srcdoc"></iframe>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_WebExtensionContentScript.js
@@ -0,0 +1,176 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const {newURI} = Services.io;
+
+const server = createHttpServer();
+server.registerDirectory("/data/", do_get_file("data"));
+
+let policy = new WebExtensionPolicy({
+  id: "foo@bar.baz",
+  mozExtensionHostname: "88fb51cd-159f-4859-83db-7065485bc9b2",
+  baseURL: "file:///foo",
+
+  allowedOrigins: new MatchPatternSet([]),
+  localizeCallback() {},
+});
+
+add_task(async function test_WebExtensinonContentScript_url_matching() {
+  let contentScript = new WebExtensionContentScript(policy, {
+    matches: new MatchPatternSet(["http://foo.com/bar", "*://bar.com/baz/*"]),
+
+    excludeMatches: new MatchPatternSet(["*://bar.com/baz/quux"]),
+
+    includeGlobs: ["*flerg*", "*.com/bar", "*/quux"].map(glob => new MatchGlob(glob)),
+
+    excludeGlobs: ["*glorg*"].map(glob => new MatchGlob(glob)),
+  });
+
+  ok(contentScript.matchesURI(newURI("http://foo.com/bar")),
+     "Simple matches include should match");
+
+  ok(contentScript.matchesURI(newURI("https://bar.com/baz/xflergx")),
+     "Simple matches include should match");
+
+  ok(!contentScript.matchesURI(newURI("https://bar.com/baz/xx")),
+     "Failed includeGlobs match pattern should not match");
+
+  ok(!contentScript.matchesURI(newURI("https://bar.com/baz/quux")),
+     "Excluded match pattern should not match");
+
+  ok(!contentScript.matchesURI(newURI("https://bar.com/baz/xflergxglorgx")),
+     "Excluded match glob should not match");
+});
+
+async function loadURL(url, {frameCount}) {
+  let windows = new Map();
+  let requests = new Map();
+
+  let resolveLoad;
+  let loadPromise = new Promise(resolve => { resolveLoad = resolve; });
+
+  function requestObserver(request) {
+    request.QueryInterface(Ci.nsIChannel);
+    if (request.isDocument) {
+      requests.set(request.name, request);
+    }
+  }
+  function loadObserver(window) {
+    windows.set(window.location.href, window);
+    if (windows.size == frameCount) {
+      resolveLoad();
+    }
+  }
+
+  Services.obs.addObserver(requestObserver, "http-on-examine-response");
+  Services.obs.addObserver(loadObserver, "content-document-global-created");
+
+  let webNav = Services.appShell.createWindowlessBrowser(false);
+  webNav.loadURI(url, 0, null, null, null);
+
+  await loadPromise;
+
+  Services.obs.removeObserver(requestObserver, "http-on-examine-response");
+  Services.obs.removeObserver(loadObserver, "content-document-global-created");
+
+  return {webNav, windows, requests};
+}
+
+add_task(async function test_WebExtensinonContentScript_frame_matching() {
+  if (AppConstants.platform == "linux") {
+    // The windowless browser currently does not load correctly on Linux on
+    // infra.
+    return;
+  }
+
+  let baseURL = `http://localhost:${server.identity.primaryPort}/data`;
+  let urls = {
+    topLevel: `${baseURL}/file_toplevel.html`,
+    iframe: `${baseURL}/file_iframe.html`,
+    srcdoc: "about:srcdoc",
+    aboutBlank: "about:blank",
+  };
+
+  let {webNav, windows, requests} = await loadURL(urls.topLevel, {frameCount: 4});
+
+  let tests = [
+    {
+      contentScript: {
+        matches: new MatchPatternSet(["http://localhost/data/*"]),
+      },
+      topLevel: true,
+      iframe: false,
+      aboutBlank: false,
+      srcdoc: false,
+    },
+
+    {
+      contentScript: {
+        matches: new MatchPatternSet(["http://localhost/data/*"]),
+        frameID: 0,
+      },
+      topLevel: true,
+      iframe: false,
+      aboutBlank: false,
+      srcdoc: false,
+    },
+
+    {
+      contentScript: {
+        matches: new MatchPatternSet(["http://localhost/data/*"]),
+        allFrames: true,
+      },
+      topLevel: true,
+      iframe: true,
+      aboutBlank: false,
+      srcdoc: false,
+    },
+
+    {
+      contentScript: {
+        matches: new MatchPatternSet(["http://localhost/data/*"]),
+        allFrames: true,
+        matchAboutBlank: true,
+      },
+      topLevel: true,
+      iframe: true,
+      aboutBlank: true,
+      srcdoc: true,
+    },
+
+    {
+      contentScript: {
+        matches: new MatchPatternSet(["http://foo.com/data/*"]),
+        allFrames: true,
+        matchAboutBlank: true,
+      },
+      topLevel: false,
+      iframe: false,
+      aboutBlank: false,
+      srcdoc: false,
+    },
+  ];
+
+  for (let [i, test] of tests.entries()) {
+    let contentScript = new WebExtensionContentScript(policy, test.contentScript);
+
+    for (let [frame, url] of Object.entries(urls)) {
+      let should = test[frame] ? "should" : "should not";
+
+      equal(contentScript.matchesWindow(windows.get(url)),
+            test[frame],
+            `Script ${i} ${should} match the ${frame} frame`);
+
+      if (url.startsWith("http")) {
+        let request = requests.get(url);
+
+        equal(contentScript.matchesLoadInfo(request.URI, request.loadInfo),
+              test[frame],
+              `Script ${i} ${should} match the request LoadInfo for ${frame} frame`);
+      }
+    }
+  }
+
+  webNav.close();
+});
--- 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_WebExtensionContentScript.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]