From 536619ee89b0bce736bfacc52062b0fcca7dc280 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 22 May 2026 12:35:37 -0400 Subject: [PATCH 1/7] Issue #3053 - Implement CSSStyleSheet constructor --- dom/webidl/CSSStyleSheet.webidl | 1 + layout/style/StyleSheet.cpp | 36 +++++++++++++++++ layout/style/StyleSheet.h | 2 + layout/style/test/mochitest.ini | 1 + .../test/test_constructed_stylesheet.html | 39 +++++++++++++++++++ 5 files changed, 79 insertions(+) create mode 100644 layout/style/test/test_constructed_stylesheet.html 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/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/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 + + + + +

+
+
+

From 6e43835b34fab60dfe13c887b0ba401998979fb1 Mon Sep 17 00:00:00 2001
From: Moonchild 
Date: Fri, 29 May 2026 23:53:22 +0200
Subject: [PATCH 2/7] Issue #3109 - Use CSS' internal length clamp value
 instead.

---
 layout/style/nsRuleNode.cpp | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/layout/style/nsRuleNode.cpp b/layout/style/nsRuleNode.cpp
index 780f62c1e9..7479e34344 100644
--- a/layout/style/nsRuleNode.cpp
+++ b/layout/style/nsRuleNode.cpp
@@ -1788,6 +1788,29 @@ SetFactor(const nsCSSValue& aValue, float& aField, RuleNodeCacheConditions& aCon
     }
     return;
 
+  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,6 +8153,7 @@ 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);

From 3ad2791ea1d2eef8f8460b2542013db2474ef2b1 Mon Sep 17 00:00:00 2001
From: Moonchild 
Date: Fri, 29 May 2026 22:53:43 +0200
Subject: [PATCH 3/7] Issue #3109 - Clamp border-radius value to Int32.MaxValue

This avoids artifacts from out-of-bounds values.
Resolves #3109
---
 layout/style/nsRuleNode.cpp | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/layout/style/nsRuleNode.cpp b/layout/style/nsRuleNode.cpp
index 7479e34344..d6c16296fd 100644
--- a/layout/style/nsRuleNode.cpp
+++ b/layout/style/nsRuleNode.cpp
@@ -8157,11 +8157,14 @@ nsRuleNode::ComputeBorderData(void* aStartStruct,
     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,

From 89b2dcbb96fea5d04e1b48903784a4d044a0bb61 Mon Sep 17 00:00:00 2001
From: wuggy 
Date: Mon, 14 Sep 2026 13:57:28 -0700
Subject: [PATCH 4/7] Fix build error

---
 layout/style/nsRuleNode.cpp | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/layout/style/nsRuleNode.cpp b/layout/style/nsRuleNode.cpp
index d6c16296fd..063e6cc10e 100644
--- a/layout/style/nsRuleNode.cpp
+++ b/layout/style/nsRuleNode.cpp
@@ -1788,6 +1788,25 @@ 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);

From 3837792171db7e7027b76afac5c9e47c01a93365 Mon Sep 17 00:00:00 2001
From: wuggy 
Date: Mon, 14 Sep 2026 14:15:03 -0700
Subject: [PATCH 5/7] (attempt) to fix ubO latest (not PM ubO)

---
 dom/script/ScriptLoader.cpp | 7 +++++++
 1 file changed, 7 insertions(+)

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;
 }
 

From bb0adb61aeb8bcb5a34fd13ea4913ccb2c17b912 Mon Sep 17 00:00:00 2001
From: wuggy 
Date: Mon, 14 Sep 2026 14:37:34 -0700
Subject: [PATCH 6/7] waiter waiter more webextensions please

---
 browser/base/content/browser.js               |  3 +
 browser/base/content/content.js               |  4 +-
 browser/base/content/nsContextMenu.js         |  2 +
 browser/base/content/utilityOverlay.js        | 29 +++++++-
 browser/modules/ContentClick.jsm              |  2 +
 .../windowwatcher/nsWindowWatcher.cpp         | 24 ++++++
 .../webextensions/ext-webNavigation.js        | 16 +++-
 .../webextensions/schemas/web_navigation.json |  1 -
 toolkit/modules/addons/WebNavigation.jsm      | 73 ++++++++++++++++++-
 .../modules/addons/WebNavigationContent.js    | 52 +++++++++++++
 10 files changed, 198 insertions(+), 8 deletions(-)

diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index 64bab110d0..8c97910941 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.getBrowserForDocument(doc),
+    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/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/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/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/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();
   }
 });

From f628239ef33625be41312b3cb64360d62b323492 Mon Sep 17 00:00:00 2001
From: wuggy 
Date: Mon, 14 Sep 2026 15:06:27 -0700
Subject: [PATCH 7/7] finally finish webRequest

---
 browser/base/content/browser.js               |  2 +-
 dom/webidl/ChannelWrapper.webidl              |  2 +-
 .../webextensions/ext-webRequest.js           | 23 ++---
 .../test_webnavigation_created_target.js      | 50 ++++++++++
 .../test/xpcshell/test_webrequest_backend.js  | 66 +++++++++++++
 .../webextensions/test/xpcshell/xpcshell.ini  |  2 +
 .../webextensions/webrequest/SecurityInfo.jsm |  6 +-
 .../webextensions/webrequest/WebRequest.jsm   | 96 +++++++++++--------
 .../webrequest/WebRequestUpload.jsm           | 15 ++-
 9 files changed, 195 insertions(+), 67 deletions(-)
 create mode 100644 toolkit/components/webextensions/test/xpcshell/test_webnavigation_created_target.js
 create mode 100644 toolkit/components/webextensions/test/xpcshell/test_webrequest_backend.js

diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index 8c97910941..8e74dd1589 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -5466,7 +5466,7 @@ function handleLinkClick(event, href, linkNode) {
   urlSecurityCheck(href, doc.nodePrincipal);
   let params = {
     charset: doc.characterSet,
-    currentBrowser: gBrowser.getBrowserForDocument(doc),
+    currentBrowser: gBrowser.getBrowserForContentWindow(doc.defaultView.top),
     frameOuterWindowID: doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIDOMWindowUtils).outerWindowID,
     allowMixedContent: persistAllowMixedContentInChildTab,
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/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/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: "" };