diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 64bab110d0..8e74dd1589 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -5466,6 +5466,9 @@ function handleLinkClick(event, href, linkNode) { urlSecurityCheck(href, doc.nodePrincipal); let params = { charset: doc.characterSet, + currentBrowser: gBrowser.getBrowserForContentWindow(doc.defaultView.top), + frameOuterWindowID: doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils).outerWindowID, allowMixedContent: persistAllowMixedContentInChildTab, referrerURI: referrerURI, referrerPolicy: referrerPolicy, diff --git a/browser/base/content/content.js b/browser/base/content/content.js index 4b5bb9ac4c..ca7458616a 100644 --- a/browser/base/content/content.js +++ b/browser/base/content/content.js @@ -442,7 +442,9 @@ var ClickEventHandler = { } } - let json = { button: event.button, shiftKey: event.shiftKey, + let json = { frameOuterWindowID: ownerDoc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils).outerWindowID, + button: event.button, shiftKey: event.shiftKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey, altKey: event.altKey, href: null, title: null, bookmark: false, referrerPolicy: referrerPolicy, diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js index 61740a1599..41e1133da9 100644 --- a/browser/base/content/nsContextMenu.js +++ b/browser/base/content/nsContextMenu.js @@ -938,6 +938,8 @@ nsContextMenu.prototype = { _openLinkInParameters : function (extra) { let params = { charset: gContextMenuContentData.charSet, + currentBrowser: this.browser, + frameOuterWindowID: this.frameOuterWindowID, originPrincipal: this.principal, triggeringPrincipal: this.principal, referrerURI: gContextMenuContentData.documentURIObject, diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js index fa2ebeb0a4..5022116510 100644 --- a/browser/base/content/utilityOverlay.js +++ b/browser/base/content/utilityOverlay.js @@ -273,6 +273,18 @@ function openLinkIn(url, where, params) { // Note that if |w| is null we might have no current browser (we'll open a new window). var aCurrentBrowser = params.currentBrowser || (w && w.gBrowser.selectedBrowser); + // Capture the source before opening a foreground tab changes the selection. + function notifyNavigationTarget(createdTabBrowser) { + if (params.frameOuterWindowID && aCurrentBrowser) { + Services.obs.notifyObservers({wrappedJSObject: { + url, + createdTabBrowser, + sourceTabBrowser: aCurrentBrowser, + sourceFrameOuterWindowID: params.frameOuterWindowID, + }}, "webNavigation-createdNavigationTarget", null); + } + } + if (where == "save") { // TODO(1073187): propagate referrerPolicy. @@ -365,7 +377,21 @@ function openLinkIn(url, where, params) { features += ",private"; } - Services.ww.openWindow(w || window, getBrowserURL(), null, features, sa); + let newWindow = Services.ww.openWindow(w || window, getBrowserURL(), null, features, sa); + if (params.frameOuterWindowID && aCurrentBrowser) { + let cleanup = () => { + Services.obs.removeObserver(observer, "browser-delayed-startup-finished"); + newWindow.removeEventListener("unload", cleanup); + }; + let observer = subject => { + if (subject == newWindow) { + cleanup(); + notifyNavigationTarget(newWindow.gBrowser.selectedBrowser); + } + }; + Services.obs.addObserver(observer, "browser-delayed-startup-finished", false); + newWindow.addEventListener("unload", cleanup); + } return; } @@ -474,6 +500,7 @@ function openLinkIn(url, where, params) { triggeringPrincipal: aTriggeringPrincipal, }); browserUsedForLoad = tabUsedForLoad.linkedBrowser; + notifyNavigationTarget(browserUsedForLoad); break; } diff --git a/browser/modules/ContentClick.jsm b/browser/modules/ContentClick.jsm index 40101d5d35..89a744f940 100644 --- a/browser/modules/ContentClick.jsm +++ b/browser/modules/ContentClick.jsm @@ -79,6 +79,8 @@ var ContentClick = { let params = { charset: browser.characterSet, + currentBrowser: browser, + frameOuterWindowID: json.frameOuterWindowID, referrerURI: browser.documentURI, referrerPolicy: json.referrerPolicy, noReferrer: json.noReferrer, diff --git a/dom/script/ScriptLoader.cpp b/dom/script/ScriptLoader.cpp index acf5027949..3b00fc76f4 100644 --- a/dom/script/ScriptLoader.cpp +++ b/dom/script/ScriptLoader.cpp @@ -2848,6 +2848,13 @@ IsInternalURIScheme(nsIURI* uri) return true; } + // Extension channels resolve to file: or jar:file: internally. Module + // imports must retain the public extension origin as their base URL. + bool isExtension; + if (NS_SUCCEEDED(uri->SchemeIs("moz-extension", &isExtension)) && isExtension) { + return true; + } + return false; } diff --git a/dom/webidl/CSSStyleSheet.webidl b/dom/webidl/CSSStyleSheet.webidl index 45ef840208..3cd4575934 100644 --- a/dom/webidl/CSSStyleSheet.webidl +++ b/dom/webidl/CSSStyleSheet.webidl @@ -13,6 +13,7 @@ enum CSSStyleSheetParsingMode { "agent" }; +[Constructor] interface CSSStyleSheet : StyleSheet { [Pure] readonly attribute CSSRule? ownerRule; diff --git a/dom/webidl/ChannelWrapper.webidl b/dom/webidl/ChannelWrapper.webidl index 8a64dcde23..95b42f9111 100644 --- a/dom/webidl/ChannelWrapper.webidl +++ b/dom/webidl/ChannelWrapper.webidl @@ -38,7 +38,7 @@ enum MozContentPolicyType { * A thin wrapper around nsIChannel and nsIHttpChannel that allows JS * callers to access them without XPConnect overhead. */ -[ChromeOnly, Exposed=Window] +[ChromeOnly, Exposed=(Window,System)] interface ChannelWrapper : EventTarget { /** * Returns the wrapper instance for the given channel. The same wrapper is diff --git a/embedding/components/windowwatcher/nsWindowWatcher.cpp b/embedding/components/windowwatcher/nsWindowWatcher.cpp index aa4d5e3914..b554c08d98 100644 --- a/embedding/components/windowwatcher/nsWindowWatcher.cpp +++ b/embedding/components/windowwatcher/nsWindowWatcher.cpp @@ -6,6 +6,7 @@ //#define USEWEAKREFS // (haven't quite figured that out yet) #include "nsWindowWatcher.h" +#include "nsHashPropertyBag.h" #include "nsAutoWindowStateHelper.h" #include "nsCRT.h" @@ -1212,6 +1213,29 @@ nsWindowWatcher::OpenWindowInternal(mozIDOMWindowProxy* aParent, // userContextId. MOZ_ASSERT(CheckUserContextCompatibility(newDocShell)); + // If this tab or window has been opened by a window.open call, we have to provide + // all the data needed to send a webNavigation.onCreatedNavigationTarget event. + if (windowIsNew && parentDocShell && newDocShellItem) { + nsCOMPtr obsSvc = + mozilla::services::GetObserverService(); + + if (obsSvc) { + RefPtr props = new nsHashPropertyBag(); + + if (uriToLoad) { + // The url notified in the webNavigation.onCreatedNavigationTarget event. + props->SetPropertyAsACString(NS_LITERAL_STRING("url"), + uriToLoad->GetSpecOrDefault()); + } + + props->SetPropertyAsInterface(NS_LITERAL_STRING("sourceTabDocShell"), parentDocShell); + props->SetPropertyAsInterface(NS_LITERAL_STRING("createdTabDocShell"), newDocShellItem); + + obsSvc->NotifyObservers(static_cast(props), + "webNavigation-createdNavigationTarget-from-js", nullptr); + } + } + if (uriToLoad && aNavigate) { newDocShell->LoadURI( uriToLoad, diff --git a/layout/style/StyleSheet.cpp b/layout/style/StyleSheet.cpp index adcd0fc01b..3c0856d840 100644 --- a/layout/style/StyleSheet.cpp +++ b/layout/style/StyleSheet.cpp @@ -12,8 +12,10 @@ #include "mozilla/CSSStyleSheet.h" #include "mozAutoDocUpdate.h" +#include "nsContentUtils.h" #include "nsIMediaList.h" #include "nsNullPrincipal.h" +#include "nsPIDOMWindow.h" using namespace mozilla::dom; @@ -222,6 +224,40 @@ StyleSheet::DeleteRule(uint32_t aIndex) // WebIDL CSSStyleSheet API +/* static */ already_AddRefed +StyleSheet::Constructor(const GlobalObject& aGlobal, ErrorResult& aRv) +{ + nsCOMPtr window = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!window) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr document = window->GetDoc(); + if (!document) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr documentURI = document->GetDocumentURI(); + nsCOMPtr baseURI = document->GetBaseURI(); + nsIPrincipal* principal = nsContentUtils::ObjectPrincipal(aGlobal.Get()); + if (!documentURI || !baseURI || !principal) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr sheet = + new CSSStyleSheet(css::eAuthorSheetFeatures, CORS_NONE, + document->GetReferrerPolicy()); + sheet->SetURIs(documentURI, nullptr, baseURI); + sheet->SetPrincipal(principal); + sheet->SetComplete(); + + return sheet.forget(); +} + dom::CSSRuleList* StyleSheet::GetCssRules(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) diff --git a/layout/style/StyleSheet.h b/layout/style/StyleSheet.h index fd1e9b3652..99ed270127 100644 --- a/layout/style/StyleSheet.h +++ b/layout/style/StyleSheet.h @@ -149,6 +149,8 @@ public: // The XPCOM SetDisabled is fine for WebIDL. // WebIDL CSSStyleSheet API + static already_AddRefed Constructor(const dom::GlobalObject& aGlobal, + ErrorResult& aRv); virtual css::Rule* GetDOMOwnerRule() const = 0; dom::CSSRuleList* GetCssRules(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv); diff --git a/layout/style/nsRuleNode.cpp b/layout/style/nsRuleNode.cpp index 780f62c1e9..063e6cc10e 100644 --- a/layout/style/nsRuleNode.cpp +++ b/layout/style/nsRuleNode.cpp @@ -1788,6 +1788,48 @@ SetFactor(const nsCSSValue& aValue, float& aField, RuleNodeCacheConditions& aCon } return; + #include "CSSCalc.h" + + struct RuleNodeReduceNumberCalcOps + : public mozilla::css::BasicFloatCalcOps + , public mozilla::css::CSSValueInputCalcOps + { + float ComputeLeafValue(const nsCSSValue& aValue) + { + MOZ_ASSERT(aValue.GetUnit() == eCSSUnit_Number, + "Expected a number-only calc expression"); + return aValue.GetFloatValue(); + } + + float ComputeNumber(const nsCSSValue& aValue) + { + return mozilla::css::ComputeCalc(aValue, *this); + } + }; + + case eCSSUnit_Calc: { + RuleNodeReduceNumberCalcOps ops; + aField = css::ComputeCalc(aValue, ops); + if (mozilla::IsNaN(aField)) { + aField = 0.0f; + } + if (aFlags & SETFCT_POSITIVE) { + NS_ASSERTION(aField >= 0.0f, "negative value for positive-only property"); + if (aField < 0.0f) { + aField = 0.0f; + } + } + if (aFlags & SETFCT_OPACITY) { + if (aField < 0.0f) { + aField = 0.0f; + } + if (aField > 1.0f) { + aField = 1.0f; + } + } + return; + } + case eCSSUnit_Inherit: aConditions.SetUncacheable(); aField = aParentValue; @@ -8130,14 +8172,18 @@ nsRuleNode::ComputeBorderData(void* aStartStruct, { const nsCSSPropertyID* subprops = nsCSSProps::SubpropertyEntryFor(eCSSProperty_border_radius); + const float RADIUS_MAX = 17895697; // CSS length clamp value NS_FOR_CSS_FULL_CORNERS(corner) { int cx = FullToHalfCorner(corner, false); int cy = FullToHalfCorner(corner, true); - const nsCSSValue& radius = *aRuleData->ValueFor(subprops[corner]); + nsCSSValue radius = *aRuleData->ValueFor(subprops[corner]); nsStyleCoord parentX = parentBorder->mBorderRadius.Get(cx); nsStyleCoord parentY = parentBorder->mBorderRadius.Get(cy); nsStyleCoord coordX, coordY; - + // Clamp border radius to the max value so it will not wrap and cause artifacts. + if (radius.GetFloatValue() > RADIUS_MAX) { + radius.SetFloatValue(RADIUS_MAX, eCSSUnit_Number); + } if (SetPairCoords(radius, coordX, coordY, parentX, parentY, SETCOORD_LPH | SETCOORD_INITIAL_ZERO | SETCOORD_STORE_CALC | SETCOORD_UNSET_INITIAL, diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini index 6e4b68e82f..4c865a6b64 100644 --- a/layout/style/test/mochitest.ini +++ b/layout/style/test/mochitest.ini @@ -164,6 +164,7 @@ support-files = file_bug1089417_iframe.html [test_condition_text.html] [test_condition_text_assignment.html] [test_contain_formatting_context.html] +[test_constructed_stylesheet.html] [test_counter_descriptor_storage.html] [test_counter_style.html] [test_css_cross_domain.html] diff --git a/layout/style/test/test_constructed_stylesheet.html b/layout/style/test/test_constructed_stylesheet.html new file mode 100644 index 0000000000..685faf3e48 --- /dev/null +++ b/layout/style/test/test_constructed_stylesheet.html @@ -0,0 +1,39 @@ + + + + + Test CSSStyleSheet constructor + + + + +

+
+
+
diff --git a/toolkit/components/webextensions/ext-webNavigation.js b/toolkit/components/webextensions/ext-webNavigation.js
index 904f3a4a78..fd318e58a8 100644
--- a/toolkit/components/webextensions/ext-webNavigation.js
+++ b/toolkit/components/webextensions/ext-webNavigation.js
@@ -115,6 +115,20 @@ function WebNavigationEventManager(context, eventName) {
         parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
       };
 
+      if (eventName == "onCreatedNavigationTarget") {
+        let source = {};
+        extensions.emit("fill-browser-data", data.sourceTabBrowser, source);
+        if (!(source.tabId >= 0)) {
+          return;
+        }
+        delete data2.frameId;
+        delete data2.parentFrameId;
+        data2.sourceTabId = source.tabId;
+        data2.sourceFrameId = ExtensionManagement.getFrameId(data.sourceWindowId);
+        // Firefox does not expose renderer process IDs through this API.
+        data2.sourceProcessId = -1;
+      }
+
       if (eventName == "onErrorOccurred") {
         data2.error = data.error;
       }
@@ -162,7 +176,7 @@ extensions.registerSchemaAPI("webNavigation", "addon_parent", context => {
       onErrorOccurred: new WebNavigationEventManager(context, "onErrorOccurred").api(),
       onReferenceFragmentUpdated: new WebNavigationEventManager(context, "onReferenceFragmentUpdated").api(),
       onHistoryStateUpdated: new WebNavigationEventManager(context, "onHistoryStateUpdated").api(),
-      onCreatedNavigationTarget: ignoreEvent(context, "webNavigation.onCreatedNavigationTarget"),
+      onCreatedNavigationTarget: new WebNavigationEventManager(context, "onCreatedNavigationTarget").api(),
       getAllFrames(details) {
         let tab = TabManager.getTab(details.tabId, context);
 
diff --git a/toolkit/components/webextensions/ext-webRequest.js b/toolkit/components/webextensions/ext-webRequest.js
index 0ae9a28127..a5b924d497 100644
--- a/toolkit/components/webextensions/ext-webRequest.js
+++ b/toolkit/components/webextensions/ext-webRequest.js
@@ -4,8 +4,6 @@ var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
-                                  "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebRequest",
                                   "resource://gre/modules/WebRequest.jsm");
 
@@ -75,11 +73,16 @@ function WebRequestEventManager(context, eventName) {
         }
       }
 
+      if (data.registerTraceableChannel) {
+        let remoteTab = context.xulBrowser && context.xulBrowser.frameLoader
+                          ? context.xulBrowser.frameLoader.remoteTab : null;
+        data.registerTraceableChannel({id: context.extension.id}, remoteTab);
+      }
       return context.runSafe(callback, data2);
     };
 
     let filter2 = {};
-    filter2.urls = new MatchPattern(filter.urls);
+    filter2.urls = filter.urls;
     if (filter.types) {
       filter2.types = filter.types;
     }
@@ -105,7 +108,9 @@ function WebRequestEventManager(context, eventName) {
       }
     }
 
-    WebRequest[eventName].addListener(listener, filter2, info2);
+    WebRequest[eventName].addListener(listener, filter2, info2, {
+      policy: {id: context.extension.id, allowedOrigins: context.extension.whiteListedHosts},
+    });
     return () => {
       WebRequest[eventName].removeListener(listener);
     };
@@ -117,14 +122,6 @@ function WebRequestEventManager(context, eventName) {
 WebRequestEventManager.prototype = Object.create(SingletonEventManager.prototype);
 
 function makeWebRequestEvent(context, eventName) {
-  if (!(eventName in WebRequest) || !WebRequest[eventName]) {
-    let name = `webRequest.${eventName}`;
-    return new SingletonEventManager(context, name, () => {
-      Cu.reportError(`webRequest.${eventName} is not supported by this runtime.`);
-      return () => {};
-    }).api();
-  }
-
   return new WebRequestEventManager(context, eventName).api();
 }
 
@@ -156,7 +153,7 @@ extensions.registerSchemaAPI("webRequest", "addon_parent", context => {
 
         return WebRequest.getSecurityInfo({
           id: requestId,
-          policy: context.extension.policy,
+          policy: {id: context.extension.id},
           remoteTab,
           options,
         });
diff --git a/toolkit/components/webextensions/schemas/web_navigation.json b/toolkit/components/webextensions/schemas/web_navigation.json
index 1e13b181ac..3fda550e91 100644
--- a/toolkit/components/webextensions/schemas/web_navigation.json
+++ b/toolkit/components/webextensions/schemas/web_navigation.json
@@ -284,7 +284,6 @@
       },
       {
         "name": "onCreatedNavigationTarget",
-        "unsupported": true,
         "type": "function",
         "description": "Fired when a new window, or a new tab in an existing window, is created to host a navigation.",
         "parameters": [
diff --git a/toolkit/components/webextensions/test/xpcshell/test_webnavigation_created_target.js b/toolkit/components/webextensions/test/xpcshell/test_webnavigation_created_target.js
new file mode 100644
index 0000000000..6acd376836
--- /dev/null
+++ b/toolkit/components/webextensions/test/xpcshell/test_webnavigation_created_target.js
@@ -0,0 +1,50 @@
+"use strict";
+
+add_task(function* test_created_navigation_target_dispatch() {
+  let scope = {};
+  Services.scriptloader.loadSubScript("resource://gre/modules/WebNavigation.jsm", scope);
+  let {Manager, WebNavigation} = scope;
+  let event = WebNavigation.onCreatedNavigationTarget;
+  let source = {}, target = {};
+  let received = [];
+  let listener = data => received.push(data);
+  event.addListener(listener, {matches: url => url == "https://example.com/target"});
+  do_register_cleanup(() => event.removeListener(listener));
+
+  for (let sourceFirst of [true, false]) {
+    let data = {
+      url: "https://example.com/target", sourceWindowId: 123,
+      createdWindowId: sourceFirst ? 456 : 789,
+    };
+    Manager.onCreatedNavigationTarget(sourceFirst ? source : target,
+      Object.assign({isSourceTab: sourceFirst}, data));
+    equal(received.length, 0, "wait for both browsers");
+    Manager.onCreatedNavigationTarget(sourceFirst ? target : source,
+      Object.assign({isSourceTab: !sourceFirst}, data));
+    equal(received.length, 1, "dispatch once, in either message order");
+    equal(received[0].browser, target);
+    equal(received[0].sourceTabBrowser, source);
+    equal(received[0].sourceWindowId, 123);
+    received.length = 0;
+  }
+
+  function notify(url) {
+    Services.obs.notifyObservers({wrappedJSObject: {
+      url, sourceTabBrowser: source, createdTabBrowser: target,
+      sourceFrameOuterWindowID: 123,
+    }}, "webNavigation-createdNavigationTarget", null);
+  }
+  notify("https://other.example/target");
+  equal(received.length, 0, "filter applies to chrome-created targets too");
+  notify("https://example.com/target");
+  equal(received.length, 1);
+  Manager.onCreatedNavigationTarget(source, {
+    isSourceTab: true, createdWindowId: 1000, sourceWindowId: 123,
+    url: "https://example.com/target",
+  });
+  event.removeListener(listener);
+  equal(Manager.createdNavigationTargetByOuterWindowId.size, 0,
+        "unregistering releases unmatched messages and timers");
+  notify("https://example.com/target");
+  equal(received.length, 1, "removed listeners are not invoked");
+});
diff --git a/toolkit/components/webextensions/test/xpcshell/test_webrequest_backend.js b/toolkit/components/webextensions/test/xpcshell/test_webrequest_backend.js
new file mode 100644
index 0000000000..af32a38176
--- /dev/null
+++ b/toolkit/components/webextensions/test/xpcshell/test_webrequest_backend.js
@@ -0,0 +1,66 @@
+"use strict";
+
+// Exercise real channels: a successful import alone does not prove that the
+// observer, native wrapper, filters and blocking response are connected.
+add_task(function* test_webrequest_backend() {
+  let {WebRequest} = Cu.import("resource://gre/modules/WebRequest.jsm", {});
+  let server = createHttpServer();
+  let hits = 0;
+  server.registerPathHandler("/request", (request, response) => {
+    ++hits;
+    response.setStatusLine(request.httpVersion, 200, "OK");
+    response.write("allowed");
+  });
+  let url = `http://localhost:${server.identity.primaryPort}/request`;
+  function request(system = false) {
+    let uri = Services.io.newURI(url, null, null);
+    let channel = NetUtil.newChannel({
+      uri,
+      loadingPrincipal: system ? Services.scriptSecurityManager.getSystemPrincipal()
+        : Services.scriptSecurityManager.createCodebasePrincipal(uri, {}),
+      securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+      contentPolicyType: Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST,
+    });
+    return new Promise(resolve => {
+      NetUtil.asyncFetch(channel, (stream, status) => resolve(status));
+    });
+  }
+
+  let calls = 0;
+  let block = data => {
+    ++calls;
+    equal(data.url, url);
+    equal(data.type, "xmlhttprequest");
+    equal(typeof data.requestId, "string");
+    return {cancel: true};
+  };
+  let event = WebRequest.onBeforeRequest;
+  do_register_cleanup(() => event.removeListener(block));
+  event.addListener(block, {urls: ["http://localhost/*"]}, ["blocking"]);
+  equal(yield request(), Cr.NS_ERROR_ABORT, "blocking listener cancels the channel");
+  equal(calls, 1);
+  equal(hits, 0, "cancelled request never reaches the server");
+  event.removeListener(block);
+  equal(yield request(), Cr.NS_OK, "removing the listener restores loading");
+
+  for (let filter of [
+    {urls: ["http://example.org/*"]},
+    {urls: [""], types: ["image"]},
+    {urls: [""], incognito: true},
+    {urls: [""], tabId: 123},
+  ]) {
+    event.addListener(block, filter, ["blocking"]);
+    equal(yield request(), Cr.NS_OK, "nonmatching requests are untouched");
+    event.removeListener(block);
+  }
+  event.addListener(block, {urls: [""]}, ["blocking"], {
+    policy: {id: "backend-test", allowedOrigins: new (Cu.import(
+      "resource://gre/modules/MatchPattern.jsm", {}).MatchPattern)(["http://example.org/*"])},
+  });
+  equal(yield request(), Cr.NS_OK, "host permissions constrain the listener");
+  event.removeListener(block);
+  event.addListener(block, {urls: [""]}, ["blocking"]);
+  equal(yield request(true), Cr.NS_OK, "system requests are not exposed");
+  event.removeListener(block);
+  equal(calls, 1, "only the matching content request was dispatched");
+});
diff --git a/toolkit/components/webextensions/test/xpcshell/xpcshell.ini b/toolkit/components/webextensions/test/xpcshell/xpcshell.ini
index d2c6fd5d07..7abf9fe3cd 100644
--- a/toolkit/components/webextensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/webextensions/test/xpcshell/xpcshell.ini
@@ -8,6 +8,8 @@ support-files =
 tags = webextensions
 
 [test_csp_custom_policies.js]
+[test_webrequest_backend.js]
+[test_webnavigation_created_target.js]
 [test_csp_validator.js]
 [test_ext_alarms.js]
 [test_ext_alarms_does_not_fire.js]
diff --git a/toolkit/components/webextensions/webrequest/SecurityInfo.jsm b/toolkit/components/webextensions/webrequest/SecurityInfo.jsm
index 4652aa28da..daa33fa406 100644
--- a/toolkit/components/webextensions/webrequest/SecurityInfo.jsm
+++ b/toolkit/components/webextensions/webrequest/SecurityInfo.jsm
@@ -4,11 +4,11 @@
 
 "use strict";
 
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
 const EXPORTED_SYMBOLS = ["SecurityInfo"];
 
-const { XPCOMUtils } = ChromeUtils.import(
-  "resource://gre/modules/XPCOMUtils.jsm"
-);
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
 
 const wpl = Ci.nsIWebProgressListener;
 XPCOMUtils.defineLazyServiceGetter(
diff --git a/toolkit/components/webextensions/webrequest/WebRequest.jsm b/toolkit/components/webextensions/webrequest/WebRequest.jsm
index 72fd9a3f26..b5eb39abcc 100644
--- a/toolkit/components/webextensions/webrequest/WebRequest.jsm
+++ b/toolkit/components/webextensions/webrequest/WebRequest.jsm
@@ -4,6 +4,8 @@
 
 "use strict";
 
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
 const EXPORTED_SYMBOLS = ["WebRequest"];
 
 /* exported WebRequest */
@@ -12,29 +14,36 @@ const EXPORTED_SYMBOLS = ["WebRequest"];
 
 const { nsIHttpActivityObserver, nsISocketTransport } = Ci;
 
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-const { XPCOMUtils } = ChromeUtils.import(
-  "resource://gre/modules/XPCOMUtils.jsm"
-);
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
 
-XPCOMUtils.defineLazyModuleGetters(this, {
-  ExtensionParent: "resource://gre/modules/ExtensionParent.jsm",
-  ExtensionUtils: "resource://gre/modules/ExtensionUtils.jsm",
-  WebRequestUpload: "resource://gre/modules/WebRequestUpload.jsm",
-  SecurityInfo: "resource://gre/modules/SecurityInfo.jsm",
-});
 
-// WebRequest.jsm's only consumer is ext-webRequest.js, so we can depend on
-// the apiManager.global being initialized.
-XPCOMUtils.defineLazyGetter(this, "tabTracker", () => {
-  return ExtensionParent.apiManager.global.tabTracker;
-});
-XPCOMUtils.defineLazyGetter(this, "getCookieStoreIdForOriginAttributes", () => {
-  return ExtensionParent.apiManager.global.getCookieStoreIdForOriginAttributes;
-});
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent", "resource://gre/modules/ExtensionParent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils", "resource://gre/modules/ExtensionUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebRequestUpload", "resource://gre/modules/WebRequestUpload.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SecurityInfo", "resource://gre/modules/SecurityInfo.jsm");
+
+
+function getCookieStoreIdForOriginAttributes(attrs) {
+  if (attrs.privateBrowsingId) {
+    return "firefox-private";
+  }
+  return attrs.userContextId ? "firefox-container-" + attrs.userContextId : "firefox-default";
+}
+
+XPCOMUtils.defineLazyServiceGetter(this, "categoryManager",
+                                  "@mozilla.org/categorymanager;1", "nsICategoryManager");
+XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern", "resource://gre/modules/MatchPattern.jsm");
+
+function matchesRequest(channel, opts, extraData) {
+  return !channel.isSystemLoad &&
+         (!opts.urlPattern || opts.urlPattern.matches(channel.finalURI)) &&
+         (!opts.policy || opts.policy.allowedOrigins.matches(channel.finalURI)) &&
+         channel.matches(opts.filter, opts.policy ? opts.policy.id : "", extraData);
+}
 
 function runLater(job) {
-  Services.tm.dispatchToMainThread(job);
+  Services.tm.mainThread.dispatch(job, Ci.nsIThread.DISPATCH_NORMAL);
 }
 
 function parseFilter(filter) {
@@ -165,7 +174,7 @@ class HeaderChanger {
 }
 
 const checkRestrictedHeaderValue = (value, opts = {}) => {
-  let uri = Services.io.newURI(`https://${value}/`);
+  let uri = Services.io.newURI(`https://${value}/`, null, null);
   let { policy } = opts;
 
   if (policy && !policy.allowedOrigins.matches(uri)) {
@@ -281,7 +290,7 @@ var ChannelEventSink = {
   _classID: Components.ID("115062f8-92f1-11e5-8b7f-080027b0f7ec"),
   _contractID: "@mozilla.org/webrequest/channel-event-sink;1",
 
-  QueryInterface: ChromeUtils.generateQI(["nsIChannelEventSink", "nsIFactory"]),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink, Ci.nsIFactory]),
 
   init() {
     Components.manager
@@ -295,7 +304,7 @@ var ChannelEventSink = {
   },
 
   register() {
-    Services.catMan.addCategoryEntry(
+    categoryManager.addCategoryEntry(
       "net-channel-event-sinks",
       this._contractID,
       this._contractID,
@@ -305,7 +314,7 @@ var ChannelEventSink = {
   },
 
   unregister() {
-    Services.catMan.deleteCategoryEntry(
+    categoryManager.deleteCategoryEntry(
       "net-channel-event-sinks",
       this._contractID,
       false
@@ -460,7 +469,7 @@ class AuthRequestor {
     this.httpObserver.runChannelListener(wrapper, "onAuthRequired", data);
 
     return {
-      QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
       cancel() {
         try {
           callback.onAuthCancelled(context, false);
@@ -474,10 +483,10 @@ class AuthRequestor {
   }
 }
 
-AuthRequestor.prototype.QueryInterface = ChromeUtils.generateQI([
-  "nsIInterfaceRequestor",
-  "nsIAuthPromptProvider",
-  "nsIAuthPrompt2",
+AuthRequestor.prototype.QueryInterface = XPCOMUtils.generateQI([
+  Ci.nsIInterfaceRequestor,
+  Ci.nsIAuthPromptProvider,
+  Ci.nsIAuthPrompt2,
 ]);
 
 // Most WebRequest events are implemented via the observer services, but
@@ -563,21 +572,21 @@ HttpObserverManager = {
       this.listeners.onSendHeaders.size;
     if (needOpening && !this.openingInitialized) {
       this.openingInitialized = true;
-      Services.obs.addObserver(this, "http-on-modify-request");
+      Services.obs.addObserver(this, "http-on-modify-request", false);
     } else if (!needOpening && this.openingInitialized) {
       this.openingInitialized = false;
       Services.obs.removeObserver(this, "http-on-modify-request");
     }
     if (needBeforeConnect && !this.beforeConnectInitialized) {
       this.beforeConnectInitialized = true;
-      Services.obs.addObserver(this, "http-on-before-connect");
+      Services.obs.addObserver(this, "http-on-before-connect", false);
     } else if (!needBeforeConnect && this.beforeConnectInitialized) {
       this.beforeConnectInitialized = false;
       Services.obs.removeObserver(this, "http-on-before-connect");
     }
 
     let haveBlocking = Object.values(this.listeners).some(listeners =>
-      Array.from(listeners.values()).some(listener => listener.blockingAllowed)
+      Array.from(listeners.values()).some(listener => listener.blocking)
     );
 
     this.needTracing =
@@ -593,9 +602,9 @@ HttpObserverManager = {
 
     if (needExamine && !this.examineInitialized) {
       this.examineInitialized = true;
-      Services.obs.addObserver(this, "http-on-examine-response");
-      Services.obs.addObserver(this, "http-on-examine-cached-response");
-      Services.obs.addObserver(this, "http-on-examine-merged-response");
+      Services.obs.addObserver(this, "http-on-examine-response", false);
+      Services.obs.addObserver(this, "http-on-examine-cached-response", false);
+      Services.obs.addObserver(this, "http-on-examine-merged-response", false);
     } else if (!needExamine && this.examineInitialized) {
       this.examineInitialized = false;
       Services.obs.removeObserver(this, "http-on-examine-response");
@@ -692,7 +701,7 @@ HttpObserverManager = {
       // Make a trip through the event loop to make sure errors have a
       // chance to be processed before we fall back to a generic error
       // string.
-      Services.tm.dispatchToMainThread(() => {
+      runLater(() => {
         channel.errorCheck();
         if (!channel.errorString) {
           this.runChannelListener(channel, "onErrorOccurred", {
@@ -712,7 +721,7 @@ HttpObserverManager = {
   },
 
   getRequestData(channel, extraData) {
-    let originAttributes = channel.loadInfo?.originAttributes;
+    let originAttributes = channel.channel.loadInfo?.originAttributes;
     let data = {
       requestId: String(channel.id),
       url: channel.finalURL,
@@ -725,6 +734,9 @@ HttpObserverManager = {
       documentUrl: channel.documentURL || undefined,
 
       tabId: this.getBrowserData(channel).tabId,
+      browser: channel.browserElement,
+      windowId: channel.windowId,
+      isSystemPrincipal: channel.isSystemLoad,
       frameId: channel.windowId,
       parentWindowId: channel.parentWindowId,
 
@@ -783,7 +795,8 @@ HttpObserverManager = {
     let browserData = wrapper._browserData;
     if (!browserData) {
       if (wrapper.browserElement) {
-        browserData = tabTracker.getBrowserData(wrapper.browserElement);
+        browserData = {};
+        ExtensionParent.apiManager.emit("fill-browser-data", wrapper.browserElement, browserData);
       } else {
         browserData = { tabId: -1, windowId: -1 };
       }
@@ -815,7 +828,7 @@ HttpObserverManager = {
             return;
           }
         }
-        if (!channel.matches(opts.filter, opts.policy ? opts.policy.id : "", extraData)) {
+        if (!matchesRequest(channel, opts, extraData)) {
           return;
         }
 
@@ -957,7 +970,7 @@ HttpObserverManager = {
         if (result.redirectUrl) {
           try {
             channel.suspended = false;
-            channel.redirectTo(Services.io.newURI(result.redirectUrl));
+            channel.redirectTo(Services.io.newURI(result.redirectUrl, null, null));
 
             // Web Extensions using the WebRequest API are allowed
             // to redirect a channel to a data: URI, hence we mark
@@ -967,7 +980,7 @@ HttpObserverManager = {
             // RedirectTo() implementation explicitly drops the flag
             // to avoid additional redirects not caused by the
             // Web Extension.
-            channel.loadInfo.allowInsecureRedirectToDataURI = true;
+            channel.channel.loadInfo.allowInsecureRedirectToDataURI = true;
 
             // To pass CORS checks, we pretend the current request's
             // response allows the triggering origin to access.
@@ -1047,7 +1060,7 @@ HttpObserverManager = {
     }
 
     for (let opts of listener.values()) {
-      if (channel.matches(opts.filter, opts.policy ? opts.policy.id : "", extraData)) {
+      if (matchesRequest(channel, opts, extraData)) {
         return true;
       }
     }
@@ -1096,6 +1109,7 @@ HttpEvent.prototype = {
   addListener(callback, filter = null, options = null, optionsObject = null) {
     let opts = parseExtra(options, this.options, optionsObject);
     opts.filter = parseFilter(filter);
+    opts.urlPattern = opts.filter.urls ? new MatchPattern(opts.filter.urls) : null;
     HttpObserverManager.addListener(this.internalEvent, callback, opts);
   },
 
diff --git a/toolkit/components/webextensions/webrequest/WebRequestUpload.jsm b/toolkit/components/webextensions/webrequest/WebRequestUpload.jsm
index eb8a2bc6b5..d5d862e3c5 100644
--- a/toolkit/components/webextensions/webrequest/WebRequestUpload.jsm
+++ b/toolkit/components/webextensions/webrequest/WebRequestUpload.jsm
@@ -4,21 +4,19 @@
 
 "use strict";
 
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
 const EXPORTED_SYMBOLS = ["WebRequestUpload"];
 
 /* exported WebRequestUpload */
 
-const { XPCOMUtils } = ChromeUtils.import(
-  "resource://gre/modules/XPCOMUtils.jsm"
-);
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
 
-const { ExtensionUtils } = ChromeUtils.import(
-  "resource://gre/modules/ExtensionUtils.jsm"
-);
+const { ExtensionUtils } = Cu.import("resource://gre/modules/ExtensionUtils.jsm", {});
 
 const { DefaultMap } = ExtensionUtils;
 
-XPCOMUtils.defineLazyGlobalGetters(this, ["TextEncoder"]);
+Cu.importGlobalProperties(["TextEncoder"]);
 
 XPCOMUtils.defineLazyServiceGetter(
   this,
@@ -475,7 +473,8 @@ function* getRawDataChunked(
     // the file, rather than its data.
     if (
       unbuffered instanceof Ci.nsIFileInputStream ||
-      unbuffered instanceof Ci.mozIRemoteLazyInputStream
+      ("mozIRemoteLazyInputStream" in Ci &&
+       unbuffered instanceof Ci.mozIRemoteLazyInputStream)
     ) {
       // But this is not actually supported yet.
       yield { file: "" };
diff --git a/toolkit/modules/addons/WebNavigation.jsm b/toolkit/modules/addons/WebNavigation.jsm
index 6302a9d790..3754e13692 100644
--- a/toolkit/modules/addons/WebNavigation.jsm
+++ b/toolkit/modules/addons/WebNavigation.jsm
@@ -12,6 +12,7 @@ const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
                                   "resource:///modules/RecentWindow.jsm");
@@ -21,8 +22,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
 // e.g. nsNavHistory::CheckIsRecentEvent, but with a lower threshold value).
 const RECENT_DATA_THRESHOLD = 5 * 1000000;
 
-// TODO:
-// onCreatedNavigationTarget
 
 var Manager = {
   // Map[string -> Map[listener -> URLFilter]]
@@ -32,7 +31,10 @@ var Manager = {
     // Collect recent tab transition data in a WeakMap:
     //   browser -> tabTransitionData
     this.recentTabTransitionData = new WeakMap();
+    this.createdNavigationTargetByOuterWindowId = new Map();
     Services.obs.addObserver(this, "autocomplete-did-enter-text", true);
+    Services.obs.addObserver(this, "webNavigation-createdNavigationTarget", false);
+    Services.mm.addMessageListener("Extension:CreatedNavigationTarget", this);
 
     Services.mm.addMessageListener("Content:Click", this);
     Services.mm.addMessageListener("Extension:DOMContentLoaded", this);
@@ -45,7 +47,13 @@ var Manager = {
 
   uninit() {
     // Stop collecting recent tab transition data and reset the WeakMap.
-    Services.obs.removeObserver(this, "autocomplete-did-enter-text", true);
+    Services.obs.removeObserver(this, "autocomplete-did-enter-text");
+    Services.obs.removeObserver(this, "webNavigation-createdNavigationTarget");
+    Services.mm.removeMessageListener("Extension:CreatedNavigationTarget", this);
+    for (let pending of this.createdNavigationTargetByOuterWindowId.values()) {
+      clearTimeout(pending.timer);
+    }
+    this.createdNavigationTargetByOuterWindowId.clear();
     this.recentTabTransitionData = new WeakMap();
 
     Services.mm.removeMessageListener("Content:Click", this);
@@ -102,6 +110,22 @@ var Manager = {
   observe: function(subject, topic, data) {
     if (topic == "autocomplete-did-enter-text") {
       this.onURLBarAutoCompletion(subject);
+    } else if (topic == "webNavigation-createdNavigationTarget") {
+      // The observed notification is coming from privileged JavaScript components running
+      // in the main process (e.g. when a new tab or window is opened using the context menu
+      // or Ctrl/Shift + click on a link).
+      const {
+        createdTabBrowser,
+        url,
+        sourceFrameOuterWindowID,
+        sourceTabBrowser,
+      } = subject.wrappedJSObject;
+
+      this.fire("onCreatedNavigationTarget", createdTabBrowser, {}, {
+        sourceTabBrowser,
+        sourceWindowId: sourceFrameOuterWindowID,
+        url,
+      });
     }
   },
 
@@ -241,6 +265,9 @@ var Manager = {
    */
   receiveMessage({name, data, target}) {
     switch (name) {
+      case "Extension:CreatedNavigationTarget":
+        this.onCreatedNavigationTarget(target, data);
+        break;
       case "Extension:StateChange":
         this.onStateChange(target, data);
         break;
@@ -274,6 +301,44 @@ var Manager = {
     }
   },
 
+  onCreatedNavigationTarget(browser, data) {
+    const {isSourceTab, createdWindowId, sourceWindowId, url} = data;
+
+    // Source and target frame scripts identify their browsers independently.
+    // Pair their messages by the new window's outer ID, in either arrival order.
+    const pairedMessage = this.createdNavigationTargetByOuterWindowId.get(createdWindowId);
+
+    if (!pairedMessage) {
+      // A tab can close before its frame script reports. Do not retain it forever.
+      let timer = setTimeout(() => {
+        this.createdNavigationTargetByOuterWindowId.delete(createdWindowId);
+      }, 30000);
+      this.createdNavigationTargetByOuterWindowId.set(createdWindowId, {browser, data, timer});
+      return;
+    }
+
+    if (pairedMessage.data.isSourceTab == isSourceTab) {
+      return;
+    }
+    clearTimeout(pairedMessage.timer);
+    this.createdNavigationTargetByOuterWindowId.delete(createdWindowId);
+
+    let sourceTabBrowser;
+    let createdTabBrowser;
+
+    if (isSourceTab) {
+      sourceTabBrowser = browser;
+      createdTabBrowser = pairedMessage.browser;
+    } else {
+      sourceTabBrowser = pairedMessage.browser;
+      createdTabBrowser = browser;
+    }
+
+    this.fire("onCreatedNavigationTarget", createdTabBrowser, {}, {
+      sourceTabBrowser, sourceWindowId, url,
+    });
+  },
+
   onStateChange(browser, data) {
     let stateFlags = data.stateFlags;
     if (stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
@@ -357,7 +422,7 @@ const EVENTS = [
   "onErrorOccurred",
   "onReferenceFragmentUpdated",
   "onHistoryStateUpdated",
-  // "onCreatedNavigationTarget",
+  "onCreatedNavigationTarget",
 ];
 
 var WebNavigation = {};
diff --git a/toolkit/modules/addons/WebNavigationContent.js b/toolkit/modules/addons/WebNavigationContent.js
index cea4a97b38..436ce26ba3 100644
--- a/toolkit/modules/addons/WebNavigationContent.js
+++ b/toolkit/modules/addons/WebNavigationContent.js
@@ -23,6 +23,55 @@ addMessageListener("Extension:DisableWebNavigation", () => {
   removeEventListener("DOMContentLoaded", loadListener);
 });
 
+var CreatedNavigationTargetListener = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+  init() {
+    Services.obs.addObserver(this, "webNavigation-createdNavigationTarget-from-js", false);
+  },
+  uninit() {
+    Services.obs.removeObserver(this, "webNavigation-createdNavigationTarget-from-js");
+  },
+
+  observe(subject, topic, data) {
+    if (!(subject instanceof Ci.nsIPropertyBag2)) {
+      return;
+    }
+
+    let props = subject.QueryInterface(Ci.nsIPropertyBag2);
+
+    const createdDocShell = props.getPropertyAsInterface("createdTabDocShell", Ci.nsIDocShell);
+    const sourceDocShell = props.getPropertyAsInterface("sourceTabDocShell", Ci.nsIDocShell);
+
+    const isSourceTabDescendant = sourceDocShell.sameTypeRootTreeItem === docShell;
+
+    if (docShell !== createdDocShell && docShell !== sourceDocShell &&
+        !isSourceTabDescendant) {
+      // if the createdNavigationTarget is not related to this docShell
+      // (this docShell is not the newly created docShell, it is not the source docShell,
+      // and the source docShell is not a descendant of it)
+      // there is nothing to do here and return early.
+      return;
+    }
+
+    const isSourceTab = docShell === sourceDocShell || isSourceTabDescendant;
+    const sourceWindowId = WebNavigationFrames.getWindowId(sourceDocShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow));
+    const createdWindowId = WebNavigationFrames.getWindowId(createdDocShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow));
+
+    let url = "about:blank";
+    if (props.hasKey("url")) {
+      url = props.getPropertyAsACString("url");
+    }
+
+    sendAsyncMessage("Extension:CreatedNavigationTarget", {
+      url,
+      sourceWindowId,
+      createdWindowId,
+      isSourceTab,
+    });
+  },
+};
+
 var FormSubmitListener = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                           Ci.nsIFormSubmitObserver,
@@ -256,11 +305,13 @@ var WebProgressListener = {
 var disabled = false;
 WebProgressListener.init();
 FormSubmitListener.init();
+CreatedNavigationTargetListener.init();
 addEventListener("unload", () => {
   if (!disabled) {
     disabled = true;
     WebProgressListener.uninit();
     FormSubmitListener.uninit();
+    CreatedNavigationTargetListener.uninit();
   }
 });
 addMessageListener("Extension:DisableWebNavigation", () => {
@@ -268,5 +319,6 @@ addMessageListener("Extension:DisableWebNavigation", () => {
     disabled = true;
     WebProgressListener.uninit();
     FormSubmitListener.uninit();
+    CreatedNavigationTargetListener.uninit();
   }
 });