Bug 1368102: Part 2 - Add WebExtensionContentScript bindings. r?billm,mixedpuppy,zombie draft
authorKris Maglione <maglione.k@gmail.com>
Thu, 25 May 2017 17:41:40 -0700
changeset 585211 0645629fdb4affe2b4b2344f8ef5c056afb12b23
parent 585210 ac206ed4f9b16994d71466968ea6e0dcad854e47
child 585212 ac295ae8830960d2a7d0a487091329caa7e0a318
push id61052
push usermaglione.k@gmail.com
push dateFri, 26 May 2017 17:14:32 +0000
reviewersbillm, mixedpuppy, zombie
bugs1368102
milestone55.0a1
Bug 1368102: Part 2 - Add WebExtensionContentScript bindings. r?billm,mixedpuppy,zombie 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
@@ -2004,16 +2004,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
@@ -1100,16 +1100,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',
 },
@@ -1712,16 +1716,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-names, 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
@@ -938,16 +938,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,13 +1,14 @@
 /* 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
@@ -156,20 +156,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 "jsapi.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,15 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #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 {
 
@@ -105,16 +107,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,
@@ -259,24 +271,225 @@ 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)
 
 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() && aDoc.FrameID() != mFrameID.Value()) {
+    return false;
+  }
+
+  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,24 +20,28 @@
 #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)
 
+  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
@@ -46,16 +46,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,157 @@
+/* -*- 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/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() {
+  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/*"]),
+        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();
+  void requests;
+});
--- 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]