From c8460ae3e7135263cccffc2d8b42fdc449a2ea46 Mon Sep 17 00:00:00 2001 From: wuggy Date: Fri, 20 Mar 2026 10:18:45 +0000 Subject: [PATCH 1/6] Cloudflare Image Resizing fix --- modules/libpref/init/all.js | 6 ++++ netwerk/base/nsStandardURL.cpp | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 9bb2b0031d..b2511aecf3 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -1973,6 +1973,12 @@ pref("network.predictor.max-resources-per-entry", 100); pref("network.predictor.max-uri-length", 500); pref("network.predictor.cleaned-up", false); +// Cloudflare Image Resizing compatibility. +// When enabled, URLs containing the "/cdn-cgi/image/" marker will have +// everything after that marker treated as opaque path data. This matches +// Cloudflare's expectations for Image Resizing URLs. +pref("network.url.cloudflare_image_resizing.enabled", true); + // The following prefs pertain to the negotiate-auth extension (see bug 17578), // which provides transparent Kerberos or NTLM authentication using the SPNEGO // protocol. Each pref is a comma-separated list of keys, where each key has diff --git a/netwerk/base/nsStandardURL.cpp b/netwerk/base/nsStandardURL.cpp index 9334def6c7..92dce5423e 100644 --- a/netwerk/base/nsStandardURL.cpp +++ b/netwerk/base/nsStandardURL.cpp @@ -26,6 +26,7 @@ #include "prprf.h" #include "nsReadableUtils.h" #include "nsPrintfCString.h" +#include "mozilla/Preferences.h" //fixes up dependency issues in non-unified building using mozilla::dom::EncodingUtils; using namespace mozilla::ipc; @@ -1105,6 +1106,58 @@ nsStandardURL::ParseURL(const char *spec, int32_t specLen) nsresult nsStandardURL::ParsePath(const char *spec, uint32_t pathPos, int32_t pathLen) { +// Cloudflare Image Resizing compatibility (pref-controlled) +// +// This feature detects the "/cdn-cgi/image/" marker in the URL path and +// treats everything after it as opaque path data. Cloudflare's Image +// Resizing service expects clients to preserve the entire suffix exactly. +// +// Because this code runs in a hot path (URL parsing), we avoid calling +// Preferences::GetBool() repeatedly. Instead, we use AddBoolVarCache() +// to cache the pref value once and read it cheaply thereafter. + +// Cached preference: true = enable Cloudflare Image Resizing fixup +static bool sCloudflareImageResizingEnabled = true; +static bool sCloudflareImageResizingPrefCached = false; + +if (!sCloudflareImageResizingPrefCached) { + Preferences::AddBoolVarCache( + &sCloudflareImageResizingEnabled, + "network.url.cloudflare_image_resizing.enabled", + true // default if pref does not exist + ); + sCloudflareImageResizingPrefCached = true; +} + +if (sCloudflareImageResizingEnabled) { + + // Extract the full path substring from the full URL spec. + nsDependentCSubstring fullPath(spec + pathPos, pathLen); + + // Prepare iterators for scanning the path. + nsACString::const_iterator begin, end; + fullPath.BeginReading(begin); + fullPath.EndReading(end); + + // Search for the Cloudflare Image Resizing marker. + nsACString::const_iterator cfPos = begin; + if (FindInReadable(NS_LITERAL_CSTRING("/cdn-cgi/image/"), cfPos, end)) { + + // Compute how far into the path the marker was found. + uint32_t offset = cfPos.get() - begin.get(); + + // Rewrite the internal path representation so that the path + // begins at the Cloudflare marker. Everything before it is ignored. + mPath.mPos = pathPos + offset; + mPath.mLen = pathLen - offset; + + // We handled the path; no further parsing needed. + return NS_OK; + } +} + + + LOG(("ParsePath: %s pathpos %d len %d\n",spec,pathPos,pathLen)); if (pathLen > net_GetURLMaxLength()) { From a704425358747a0f000d5009fb85082a7de19f98 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sun, 22 Mar 2026 16:48:43 -0400 Subject: [PATCH 2/6] Issue #3016 - allow url.CanParse to use custom-scheme bases to match current spec --- dom/url/URL.cpp | 108 ++++- dom/url/tests/test_url.html | 826 +++++++++++++++++---------------- dom/url/tests/urlApi_worker.js | 396 +++++++++------- 3 files changed, 719 insertions(+), 611 deletions(-) diff --git a/dom/url/URL.cpp b/dom/url/URL.cpp index d33860ab82..5acaeb46fb 100644 --- a/dom/url/URL.cpp +++ b/dom/url/URL.cpp @@ -57,7 +57,84 @@ CreateObjectURLInternal(const GlobalObject& aGlobal, T aObject, CopyASCIItoUTF16(url, aResult); } -// The URL implementation for the main-thread +bool +IsRootRelativePathInput(const nsAString& aURL) +{ + return !aURL.IsEmpty() && aURL.CharAt(0) == '/' && + (aURL.Length() == 1 || aURL.CharAt(1) != '/'); +} + +bool +BuildRootRelativeFallbackSpec(const nsAString& aURL, nsIURI* aBase, + nsACString& aFallbackSpec) +{ + MOZ_ASSERT(aBase); + MOZ_ASSERT(IsRootRelativePathInput(aURL)); + + nsAutoCString baseSpec; + if (NS_FAILED(aBase->GetSpec(baseSpec)) || baseSpec.IsEmpty()) { + return false; + } + + int32_t colon = baseSpec.FindChar(':'); + if (colon <= 0) { + return false; + } + + uint32_t afterColon = static_cast(colon + 1); + if (afterColon >= baseSpec.Length() || baseSpec.CharAt(afterColon) != '/') { + // Opaque paths (for example mailto:test@example.com) are not valid bases + // for root-relative URL input. + return false; + } + + nsAutoCString input; + if (!AppendUTF16toUTF8(aURL, input, fallible)) { + return false; + } + + // Preserve authority when present (scheme://authority/path -> scheme://authority/input) + if (afterColon + 1 < baseSpec.Length() && baseSpec.CharAt(afterColon + 1) == '/') { + uint32_t authorityEnd = afterColon + 2; + while (authorityEnd < baseSpec.Length()) { + char c = baseSpec.CharAt(authorityEnd); + if (c == '/' || c == '?' || c == '#') { + break; + } + ++authorityEnd; + } + + aFallbackSpec.Assign(Substring(baseSpec, 0, authorityEnd)); + aFallbackSpec.Append(input); + return true; + } + + // Single-slash hierarchical base (scheme:/path -> scheme:/input) + aFallbackSpec.Assign(Substring(baseSpec, 0, afterColon)); + aFallbackSpec.Append(input); + return true; +} + +bool +TryResolveRootRelativeAgainstBase(const nsAString& aURL, nsIURI* aBase, + nsIURI** aOutURI) +{ + MOZ_ASSERT(aOutURI); + + if (!aBase || !IsRootRelativePathInput(aURL)) { + return false; + } + + nsAutoCString fallbackSpec; + if (!BuildRootRelativeFallbackSpec(aURL, aBase, fallbackSpec)) { + return false; + } + + nsresult rv = NS_NewURI(aOutURI, fallbackSpec, nullptr, nullptr, + nsContentUtils::GetIOService()); + return NS_SUCCEEDED(rv); +} + class URLMainThread final : public URL { public: @@ -229,7 +306,8 @@ URLMainThread::Constructor(nsISupports* aParent, const nsAString& aURL, nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, nullptr, aBase, nsContentUtils::GetIOService()); - if (NS_FAILED(rv)) { + if (NS_FAILED(rv) && + !TryResolveRootRelativeAgainstBase(aURL, aBase, getter_AddRefs(uri))) { // No need to warn in this case. It's common to use the URL constructor // to determine if a URL is valid and an exception will be propagated. aRv.ThrowTypeError(aURL); @@ -1714,30 +1792,14 @@ URL::IsValidURL(const GlobalObject& aGlobal, const nsAString& aURL, bool URL::CanParse(const GlobalObject& aGlobal, const nsAString& aURL, const Optional& aBase) { - nsCOMPtr baseUri; - if (aBase.WasPassed()) { - // Don't use NS_ConvertUTF16toUTF8 because that doesn't let us handle OOM. - nsAutoCString base; - if (!AppendUTF16toUTF8(aBase.Value(), base, fallible)) { - // Just return false with OOM errors as no ErrorResult. - return false; - } - - nsresult rv = NS_NewURI(getter_AddRefs(baseUri), base); - if (NS_FAILED(rv)) { - // Invalid base URL, return false. - return false; - } - } - - nsAutoCString urlStr; - if (!AppendUTF16toUTF8(aURL, urlStr, fallible)) { - // Just return false with OOM errors as no ErrorResult. + ErrorResult rv; + RefPtr parsed = URL::Constructor(aGlobal, aURL, aBase, rv); + if (rv.Failed() || !parsed) { + rv.SuppressException(); return false; } - nsCOMPtr uri; - return NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), urlStr, nullptr, baseUri)); + return true; } URLSearchParams* diff --git a/dom/url/tests/test_url.html b/dom/url/tests/test_url.html index 73e75667d8..a637efff25 100644 --- a/dom/url/tests/test_url.html +++ b/dom/url/tests/test_url.html @@ -1,460 +1,462 @@ - + - - - Test URL API - - - - -Mozilla Bug 887364 -Mozilla Bug 991471 -Mozilla Bug 996055 -

- -
-
- + + + + Mozilla Bug 887364 + Mozilla Bug 991471 + Mozilla Bug 996055 +

+ +

+        
 
-    if ('href' in test) is (test.href, url + '', 'stringify works');
-  }
+        
 
-  
+        
 
-  
+        
+            url.hostname = "[::192.9.5.5]";
+            is(url.hostname, "[::192.9.5.5]", "IPv6 hostname");
+            is(url.href, "http://[::192.9.5.5]/");
 
-  
 
-    url = new URL("http://localhost/");
-    url.host = "[2001::1]:30";
-    is(url.hostname, "[2001::1]", "IPv6 hostname");
-    is(url.port, "30", "Port");
-    is(url.host, "[2001::1]:30", "IPv6 host");
+        
+            var u = new URL(url);
+            ok(u.origin, "http://mochi.test:8888", "The URL generated from a blob URI has an origin");
+        
 
-  
+            var a = document.createElement("A");
+            a.href = url;
+            ok(a.origin, "http://mochi.test:8888", "The 'a' element has the correct origin");
+        
 
-  
 
-    var a = document.createElement('A');
-    a.href = url;
-    ok(a.origin, 'http://mochi.test:8888', "The 'a' element has the correct origin");
-  
+        
 
-  
+        
+            var url = new URL("..\\", base);
+            is(url.href, "http://test.com/path/");
 
-  
 
-    url = new URL("ftp:\\\\tmp\\test", base);
-    is(url.href, "ftp://tmp/test");
+        
+            var url = new URL("file:");
+            is(url.href, "file:///", "Parsing file: should work.");
 
-  
 
-    var url = new URL("file:");
-    is(url.href, "file:///", "Parsing file: should work.");
+        
+            // pathname cannot be overwritten.
+            url.pathname = "new/path?newquery#newhash";
+            is(url.href, "scheme:path/to/file?query#hash");
 
-  
 
-    url = new URL("scheme:path#hash");
-    is(url.href, "scheme:path#hash");
-    url.search = "query";
-    is(url.href, "scheme:path?query#hash");
-    url.hash = "";
-    is(url.href, "scheme:path?query");
-    url.hash = "newhash";
-    is(url.href, "scheme:path?query#newhash");
-    url.search = "";
-    is(url.href, "scheme:path#newhash");
+        
 
-    // we don't implement a spec-compliant parser yet.
-    // make sure we are bug compatible with existing implementations.
-    url = new URL("data:text/html,Link");
-    is(url.href, "data:text/html,Link");
-  
-
-  
-
+        
+    
 
diff --git a/dom/url/tests/urlApi_worker.js b/dom/url/tests/urlApi_worker.js
index a8b88e046f..bc26d8a0a9 100644
--- a/dom/url/tests/urlApi_worker.js
+++ b/dom/url/tests/urlApi_worker.js
@@ -1,212 +1,224 @@
 function ok(a, msg) {
   dump("OK: " + !!a + "  =>  " + a + " " + msg + "\n");
-  postMessage({type: 'status', status: !!a, msg: a + ": " + msg });
+  postMessage({ type: "status", status: !!a, msg: a + ": " + msg });
 }
 
 function is(a, b, msg) {
-  dump("IS: " + (a===b) + "  =>  " + a + " | " + b + " " + msg + "\n");
-  postMessage({type: 'status', status: a === b, msg: a + " === " + b + ": " + msg });
+  dump("IS: " + (a === b) + "  =>  " + a + " | " + b + " " + msg + "\n");
+  postMessage({
+    type: "status",
+    status: a === b,
+    msg: a + " === " + b + ": " + msg,
+  });
 }
 
-onmessage = function() {
+onmessage = function () {
   status = false;
   try {
-    if ((URL instanceof Object)) {
+    if (URL instanceof Object) {
       status = true;
     }
-  } catch(e) {
-  }
+  } catch (e) {}
 
   var tests = [
-    { url: 'http://www.abc.com',
+    {
+      url: "http://www.abc.com",
       base: undefined,
       error: false,
-      href: 'http://www.abc.com/',
-      origin: 'http://www.abc.com',
-      protocol: 'http:',
-      username: '',
-      password: '',
-      host: 'www.abc.com',
-      hostname: 'www.abc.com',
-      port: '',
-      pathname: '/',
-      search: '',
-      hash: ''
+      href: "http://www.abc.com/",
+      origin: "http://www.abc.com",
+      protocol: "http:",
+      username: "",
+      password: "",
+      host: "www.abc.com",
+      hostname: "www.abc.com",
+      port: "",
+      pathname: "/",
+      search: "",
+      hash: "",
     },
-    { url: 'ftp://auser:apw@www.abc.com',
+    {
+      url: "ftp://auser:apw@www.abc.com",
       base: undefined,
       error: false,
-      href: 'ftp://auser:apw@www.abc.com/',
-      origin: 'ftp://www.abc.com',
-      protocol: 'ftp:',
-      username: 'auser',
-      password: 'apw',
-      host: 'www.abc.com',
-      hostname: 'www.abc.com',
-      port: '',
-      pathname: '/',
-      search: '',
-      hash: ''
+      href: "ftp://auser:apw@www.abc.com/",
+      origin: "ftp://www.abc.com",
+      protocol: "ftp:",
+      username: "auser",
+      password: "apw",
+      host: "www.abc.com",
+      hostname: "www.abc.com",
+      port: "",
+      pathname: "/",
+      search: "",
+      hash: "",
     },
-    { url: 'http://www.abc.com:90/apath/',
+    {
+      url: "http://www.abc.com:90/apath/",
       base: undefined,
       error: false,
-      href: 'http://www.abc.com:90/apath/',
-      origin: 'http://www.abc.com:90',
-      protocol: 'http:',
-      username: '',
-      password: '',
-      host: 'www.abc.com:90',
-      hostname: 'www.abc.com',
-      port: '90',
-      pathname: '/apath/',
-      search: '',
-      hash: ''
+      href: "http://www.abc.com:90/apath/",
+      origin: "http://www.abc.com:90",
+      protocol: "http:",
+      username: "",
+      password: "",
+      host: "www.abc.com:90",
+      hostname: "www.abc.com",
+      port: "90",
+      pathname: "/apath/",
+      search: "",
+      hash: "",
     },
-    { url: 'http://www.abc.com/apath/afile.txt#ahash',
+    {
+      url: "http://www.abc.com/apath/afile.txt#ahash",
       base: undefined,
       error: false,
-      href: 'http://www.abc.com/apath/afile.txt#ahash',
-      origin: 'http://www.abc.com',
-      protocol: 'http:',
-      username: '',
-      password: '',
-      host: 'www.abc.com',
-      hostname: 'www.abc.com',
-      port: '',
-      pathname: '/apath/afile.txt',
-      search: '',
-      hash: '#ahash'
+      href: "http://www.abc.com/apath/afile.txt#ahash",
+      origin: "http://www.abc.com",
+      protocol: "http:",
+      username: "",
+      password: "",
+      host: "www.abc.com",
+      hostname: "www.abc.com",
+      port: "",
+      pathname: "/apath/afile.txt",
+      search: "",
+      hash: "#ahash",
     },
-    { url: 'http://example.com/?test#hash',
+    {
+      url: "http://example.com/?test#hash",
       base: undefined,
       error: false,
-      href: 'http://example.com/?test#hash',
-      origin: 'http://example.com',
-      protocol: 'http:',
-      username: '',
-      password: '',
-      host: 'example.com',
-      hostname: 'example.com',
-      port: '',
-      pathname: '/',
-      search: '?test',
-      hash: '#hash'
+      href: "http://example.com/?test#hash",
+      origin: "http://example.com",
+      protocol: "http:",
+      username: "",
+      password: "",
+      host: "example.com",
+      hostname: "example.com",
+      port: "",
+      pathname: "/",
+      search: "?test",
+      hash: "#hash",
     },
-    { url: 'http://example.com/?test',
+    {
+      url: "http://example.com/?test",
       base: undefined,
       error: false,
-      href: 'http://example.com/?test',
-      origin: 'http://example.com',
-      protocol: 'http:',
-      username: '',
-      password: '',
-      host: 'example.com',
-      hostname: 'example.com',
-      port: '',
-      pathname: '/',
-      search: '?test',
-      hash: ''
+      href: "http://example.com/?test",
+      origin: "http://example.com",
+      protocol: "http:",
+      username: "",
+      password: "",
+      host: "example.com",
+      hostname: "example.com",
+      port: "",
+      pathname: "/",
+      search: "?test",
+      hash: "",
     },
-    { url: 'http://example.com/carrot#question%3f',
+    {
+      url: "http://example.com/carrot#question%3f",
       base: undefined,
       error: false,
-      hash: '#question%3f'
+      hash: "#question%3f",
     },
-    { url: 'https://example.com:4443?',
+    {
+      url: "https://example.com:4443?",
       base: undefined,
       error: false,
-      protocol: 'https:',
-      port: '4443',
-      pathname: '/',
-      hash: '',
-      search: ''
+      protocol: "https:",
+      port: "4443",
+      pathname: "/",
+      hash: "",
+      search: "",
     },
-    { url: 'http://www.abc.com/apath/afile.txt#ahash?asearch',
+    {
+      url: "http://www.abc.com/apath/afile.txt#ahash?asearch",
       base: undefined,
       error: false,
-      href: 'http://www.abc.com/apath/afile.txt#ahash?asearch',
-      protocol: 'http:',
-      pathname: '/apath/afile.txt',
-      hash: '#ahash?asearch',
-      search: ''
+      href: "http://www.abc.com/apath/afile.txt#ahash?asearch",
+      protocol: "http:",
+      pathname: "/apath/afile.txt",
+      hash: "#ahash?asearch",
+      search: "",
     },
-    { url: 'http://www.abc.com/apath/afile.txt?asearch#ahash',
+    {
+      url: "http://www.abc.com/apath/afile.txt?asearch#ahash",
       base: undefined,
       error: false,
-      href: 'http://www.abc.com/apath/afile.txt?asearch#ahash',
-      protocol: 'http:',
-      pathname: '/apath/afile.txt',
-      hash: '#ahash',
-      search: '?asearch'
+      href: "http://www.abc.com/apath/afile.txt?asearch#ahash",
+      protocol: "http:",
+      pathname: "/apath/afile.txt",
+      hash: "#ahash",
+      search: "?asearch",
     },
-    { url: 'http://abc.com/apath/afile.txt?#ahash',
+    {
+      url: "http://abc.com/apath/afile.txt?#ahash",
       base: undefined,
       error: false,
-      pathname: '/apath/afile.txt',
-      hash: '#ahash',
-      search: ''
+      pathname: "/apath/afile.txt",
+      hash: "#ahash",
+      search: "",
     },
-    { url: 'http://auser:apassword@www.abc.com:90/apath/afile.txt?asearch#ahash',
+    {
+      url: "http://auser:apassword@www.abc.com:90/apath/afile.txt?asearch#ahash",
       base: undefined,
       error: false,
-      protocol: 'http:',
-      username: 'auser',
-      password: 'apassword',
-      host: 'www.abc.com:90',
-      hostname: 'www.abc.com',
-      port: '90',
-      pathname: '/apath/afile.txt',
-      hash: '#ahash',
-      search: '?asearch',
-      origin: 'http://www.abc.com:90'
+      protocol: "http:",
+      username: "auser",
+      password: "apassword",
+      host: "www.abc.com:90",
+      hostname: "www.abc.com",
+      port: "90",
+      pathname: "/apath/afile.txt",
+      hash: "#ahash",
+      search: "?asearch",
+      origin: "http://www.abc.com:90",
     },
 
-    { url: '/foo#bar',
-      base: 'www.test.org',
-      error: true,
-    },
-    { url: '/foo#bar',
-      base: null,
-      error: true,
-    },
-    { url: '/foo#bar',
-      base: 42,
-      error: true,
-    },
-    { url: 'ftp://ftp.something.net',
+    { url: "/foo#bar", base: "www.test.org", error: true },
+    { url: "/foo#bar", base: null, error: true },
+    { url: "/foo#bar", base: 42, error: true },
+    {
+      url: "ftp://ftp.something.net",
       base: undefined,
       error: false,
-      protocol: 'ftp:',
+      protocol: "ftp:",
     },
-    { url: 'file:///tmp/file',
+    {
+      url: "file:///tmp/file",
       base: undefined,
       error: false,
-      protocol: 'file:',
+      protocol: "file:",
     },
-    { url: 'gopher://gopher.something.net',
+    {
+      url: "gopher://gopher.something.net",
       base: undefined,
       error: false,
-      protocol: 'gopher:',
+      protocol: "gopher:",
     },
-    { url: 'ws://ws.something.net',
+    {
+      url: "ws://ws.something.net",
       base: undefined,
       error: false,
-      protocol: 'ws:',
+      protocol: "ws:",
     },
-    { url: 'wss://ws.something.net',
+    {
+      url: "wss://ws.something.net",
       base: undefined,
       error: false,
-      protocol: 'wss:',
+      protocol: "wss:",
     },
-    { url: 'foo://foo.something.net',
+    {
+      url: "foo://foo.something.net",
       base: undefined,
       error: false,
-      protocol: 'foo:',
+      protocol: "foo:",
     },
   ];
 
-  while(tests.length) {
+  while (tests.length) {
     var test = tests.shift();
 
     var error = false;
@@ -217,7 +229,7 @@ onmessage = function() {
       } else {
         url = new URL(test.url);
       }
-    } catch(e) {
+    } catch (e) {
       error = true;
     }
 
@@ -226,47 +238,79 @@ onmessage = function() {
       continue;
     }
 
-    if ('href' in test) is(url.href, test.href, "href");
-    if ('origin' in test) is(url.origin, test.origin, "origin");
-    if ('protocol' in test) is(url.protocol, test.protocol, "protocol");
-    if ('username' in test) is(url.username, test.username, "username");
-    if ('password' in test) is(url.password, test.password, "password");
-    if ('host' in test) is(url.host, test.host, "host");
-    if ('hostname' in test) is(url.hostname, test.hostname, "hostname");
-    if ('port' in test) is(url.port, test.port, "port");
-    if ('pathname' in test) is(url.pathname, test.pathname, "pathname");
-    if ('search' in test) is(url.search, test.search, "search");
-    if ('hash' in test) is(url.hash, test.hash, "hash");
+    if ("href" in test) is(url.href, test.href, "href");
+    if ("origin" in test) is(url.origin, test.origin, "origin");
+    if ("protocol" in test) is(url.protocol, test.protocol, "protocol");
+    if ("username" in test) is(url.username, test.username, "username");
+    if ("password" in test) is(url.password, test.password, "password");
+    if ("host" in test) is(url.host, test.host, "host");
+    if ("hostname" in test) is(url.hostname, test.hostname, "hostname");
+    if ("port" in test) is(url.port, test.port, "port");
+    if ("pathname" in test) is(url.pathname, test.pathname, "pathname");
+    if ("search" in test) is(url.search, test.search, "search");
+    if ("hash" in test) is(url.hash, test.hash, "hash");
 
-    url = new URL('https://www.example.net/what#foo?bar');
+    url = new URL("https://www.example.net/what#foo?bar");
     ok(url, "Url exists!");
 
-    if ('href' in test) url.href = test.href;
-    if ('protocol' in test) url.protocol = test.protocol;
-    if ('username' in test && test.username) url.username = test.username;
-    if ('password' in test && test.password) url.password = test.password;
-    if ('host' in test) url.host = test.host;
-    if ('hostname' in test) url.hostname = test.hostname;
-    if ('port' in test) url.port = test.port;
-    if ('pathname' in test) url.pathname = test.pathname;
-    if ('search' in test) url.search = test.search;
-    if ('hash' in test) url.hash = test.hash;
+    if ("href" in test) url.href = test.href;
+    if ("protocol" in test) url.protocol = test.protocol;
+    if ("username" in test && test.username) url.username = test.username;
+    if ("password" in test && test.password) url.password = test.password;
+    if ("host" in test) url.host = test.host;
+    if ("hostname" in test) url.hostname = test.hostname;
+    if ("port" in test) url.port = test.port;
+    if ("pathname" in test) url.pathname = test.pathname;
+    if ("search" in test) url.search = test.search;
+    if ("hash" in test) url.hash = test.hash;
 
-    if ('href' in test) is(url.href, test.href, "href");
-    if ('origin' in test) is(url.origin, test.origin, "origin");
-    if ('protocol' in test) is(url.protocol, test.protocol, "protocol");
-    if ('username' in test) is(url.username, test.username, "username");
-    if ('password' in test) is(url.password, test.password, "password");
-    if ('host' in test) is(url.host, test.host, "host");
-    if ('hostname' in test) is(test.hostname, url.hostname, "hostname");
-    if ('port' in test) is(test.port, url.port, "port");
-    if ('pathname' in test) is(test.pathname, url.pathname, "pathname");
-    if ('search' in test) is(test.search, url.search, "search");
-    if ('hash' in test) is(test.hash, url.hash, "hash");
+    if ("href" in test) is(url.href, test.href, "href");
+    if ("origin" in test) is(url.origin, test.origin, "origin");
+    if ("protocol" in test) is(url.protocol, test.protocol, "protocol");
+    if ("username" in test) is(url.username, test.username, "username");
+    if ("password" in test) is(url.password, test.password, "password");
+    if ("host" in test) is(url.host, test.host, "host");
+    if ("hostname" in test) is(test.hostname, url.hostname, "hostname");
+    if ("port" in test) is(test.port, url.port, "port");
+    if ("pathname" in test) is(test.pathname, url.pathname, "pathname");
+    if ("search" in test) is(test.search, url.search, "search");
+    if ("hash" in test) is(test.hash, url.hash, "hash");
 
-    if ('href' in test) is (test.href, url + '', 'stringify works');
+    if ("href" in test) is(test.href, url + "", "stringify works");
   }
 
-  postMessage({type: 'finish' });
-}
+  is(
+    new URL("/static/client/runtime.js", "x:/").href,
+    "x:/static/client/runtime.js",
+    "URL constructor accepts root-relative input with x:/ base",
+  );
+  ok("canParse" in URL, "URL.canParse exists");
+  ok(
+    URL.canParse("/static/client/runtime.js", "x:/"),
+    "URL.canParse accepts root-relative input with x:/ base",
+  );
+  ok(
+    URL.canParse("/static/client/runtime.js", self.location.href),
+    "URL.canParse accepts root-relative input with worker location base",
+  );
+  ok(
+    !URL.canParse("/static/client/runtime.js"),
+    "URL.canParse rejects root-relative input without base",
+  );
+  ok(
+    !URL.canParse("/static/client/runtime.js", "mailto:test@example.com"),
+    "URL.canParse rejects root-relative input against opaque mailto: base",
+  );
+  let mailtoRelativeFailed = false;
+  try {
+    new URL("/static/client/runtime.js", "mailto:test@example.com");
+  } catch (e) {
+    mailtoRelativeFailed = true;
+  }
+  ok(
+    mailtoRelativeFailed,
+    "constructor rejects root-relative input against opaque mailto: base",
+  );
 
+  postMessage({ type: "finish" });
+};

From 3d131186be4807e011e1854302414bcdde2e1cc2 Mon Sep 17 00:00:00 2001
From: Basilisk-Dev 
Date: Mon, 23 Mar 2026 08:50:01 -0400
Subject: [PATCH 3/6] Issue #2551 - implement TypedArray.prototype.with

---
 js/src/builtin/TypedArray.js                  | 59 +++++++++++++
 .../detached-array-buffer-checks.js           |  4 +
 js/src/tests/ecma_6/TypedArray/with.js        | 84 +++++++++++++++++++
 js/src/vm/TypedArrayObject.cpp                |  1 +
 4 files changed, 148 insertions(+)
 create mode 100644 js/src/tests/ecma_6/TypedArray/with.js

diff --git a/js/src/builtin/TypedArray.js b/js/src/builtin/TypedArray.js
index 237b47ad8c..9c22c51f2c 100644
--- a/js/src/builtin/TypedArray.js
+++ b/js/src/builtin/TypedArray.js
@@ -37,6 +37,10 @@ function TypedArrayLengthMethod() {
     return TypedArrayLength(this);
 }
 
+function TypedArrayContentTypeIsBigIntMethod() {
+    return IsBigInt64TypedArray(this) || IsBigUint64TypedArray(this);
+}
+
 function GetAttachedArrayBuffer(tarray) {
     var buffer = ViewedArrayBufferIfReified(tarray);
     if (IsDetachedBuffer(buffer))
@@ -895,6 +899,61 @@ function TypedArrayToReversed() {
     return A;
 }
 
+// ES2023 23.2.3.36 %TypedArray%.prototype.with ( index, value )
+function TypedArrayWith(index, value) {
+    // Step 1.
+    var O = this;
+
+    // Step 2.
+    // This function is not generic.
+    // We want to make sure that we have an attached buffer, per spec prose.
+    var isTypedArray = IsTypedArrayEnsuringArrayBuffer(O);
+
+    // If we got here, `this` is either a typed array or a wrapper for one.
+
+    // Step 3.
+    var len;
+    if (isTypedArray)
+        len = TypedArrayLength(O);
+    else
+        len = callFunction(CallTypedArrayMethodIfWrapped, O, "TypedArrayLengthMethod");
+
+    // Steps 4-6.
+    var relativeIndex = ToInteger(index);
+    var actualIndex = relativeIndex >= 0 ? relativeIndex : len + relativeIndex;
+
+    // Steps 7-8.
+    var isBigIntContentType;
+    if (isTypedArray) {
+        isBigIntContentType = callFunction(TypedArrayContentTypeIsBigIntMethod, O);
+    } else {
+        isBigIntContentType = callFunction(CallTypedArrayMethodIfWrapped, O,
+                                           "TypedArrayContentTypeIsBigIntMethod");
+    }
+    var numericValue = isBigIntContentType ? ToBigInt(value) : ToNumber(value);
+
+    // Step 9.
+    if (actualIndex < 0 || actualIndex >= len)
+        ThrowRangeError(JSMSG_BAD_INDEX);
+
+    // Step 10.
+    var A = TypedArrayCreateSameType(O, len);
+
+    // Steps 11-12.
+    for (var k = 0; k < len; k++) {
+        var fromValue;
+        if (k === actualIndex) {
+            fromValue = numericValue;
+        } else {
+            fromValue = O[k];
+        }
+        A[k] = fromValue;
+    }
+
+    // Step 13.
+    return A;
+}
+
 // ES6 draft 20150220 22.2.3.22.1 %TypedArray%.prototype.set(array [, offset])
 function SetFromNonTypedArray(target, array, targetOffset, targetLength, targetBuffer) {
     assert(!IsPossiblyWrappedTypedArray(array),
diff --git a/js/src/tests/ecma_6/TypedArray/detached-array-buffer-checks.js b/js/src/tests/ecma_6/TypedArray/detached-array-buffer-checks.js
index 76a00d886d..c5b58db328 100644
--- a/js/src/tests/ecma_6/TypedArray/detached-array-buffer-checks.js
+++ b/js/src/tests/ecma_6/TypedArray/detached-array-buffer-checks.js
@@ -97,6 +97,10 @@ assertThrowsInstanceOf(() => {
     array.values();
 }, TypeError);
 
+assertThrowsInstanceOf(() => {
+    array.with(POISON, POISON);
+}, TypeError);
+
 assertThrowsInstanceOf(() => {
     array.every(POISON);
 }, TypeError);
diff --git a/js/src/tests/ecma_6/TypedArray/with.js b/js/src/tests/ecma_6/TypedArray/with.js
new file mode 100644
index 0000000000..41ce1e706f
--- /dev/null
+++ b/js/src/tests/ecma_6/TypedArray/with.js
@@ -0,0 +1,84 @@
+for (var constructor of anyTypedArrayConstructors) {
+    assertEq(constructor.prototype.with.length, 2);
+
+    var original = new constructor([1, 2, 3, 4]);
+    var updated = original.with(1, 9);
+    assertDeepEq(updated, new constructor([1, 9, 3, 4]));
+    assertDeepEq(original, new constructor([1, 2, 3, 4]));
+    assertEq(updated === original, false);
+    assertEq(updated.constructor, constructor);
+
+    assertDeepEq(new constructor([1, 2, 3]).with(-1, 7),
+                 new constructor([1, 2, 7]));
+    assertDeepEq(new constructor([1, 2, 3]).with(-0, 7),
+                 new constructor([7, 2, 3]));
+
+    assertThrowsInstanceOf(() => {
+        new constructor([1, 2, 3]).with(3, 9);
+    }, RangeError);
+    assertThrowsInstanceOf(() => {
+        new constructor([1, 2, 3]).with(-4, 9);
+    }, RangeError);
+
+    var valueOrder = [];
+    var value = {
+        valueOf() {
+            valueOrder.push("valueOf");
+            return 9;
+        }
+    };
+    assertThrowsInstanceOf(() => {
+        new constructor([1, 2, 3]).with(9, value);
+    }, RangeError);
+    assertEq(valueOrder.join(","), "valueOf");
+
+    var ctorIgnored = new constructor([5, 6, 7]);
+    Object.defineProperty(ctorIgnored, "constructor", {
+        get() {
+            throw new Error("constructor accessor called");
+        }
+    });
+    assertDeepEq(ctorIgnored.with(0, 4), new constructor([4, 6, 7]));
+
+    if (constructor === Uint8ClampedArray ||
+        (typeof isSharedConstructor === "function" && isSharedConstructor(constructor) &&
+         constructor.name === Uint8ClampedArray.name))
+    {
+        assertDeepEq(new constructor([0, 1, 2]).with(1, 2.6),
+                     new constructor([0, 3, 2]));
+    }
+
+    var invalidReceivers = [undefined, null, 1, false, "", Symbol(), [], {}, /./,
+                            new Proxy(new constructor(), {})];
+    invalidReceivers.forEach(invalidReceiver => {
+        assertThrowsInstanceOf(() => {
+            constructor.prototype.with.call(invalidReceiver, 0, 1);
+        }, TypeError,
+        "Assert that with fails if this value is not a TypedArray");
+    });
+}
+
+for (var constructor of typedArrayConstructors) {
+    if (typeof newGlobal === "function") {
+        var withFn = newGlobal()[constructor.name].prototype.with;
+        var original = new constructor([3, 2, 1]);
+        var updated = withFn.call(original, 1, 8);
+
+        assertDeepEq(updated, new constructor([3, 8, 1]));
+        assertDeepEq(original, new constructor([3, 2, 1]));
+    }
+}
+
+if (typeof BigInt64Array === "function" && typeof BigUint64Array === "function") {
+    var bigIntArray = new BigInt64Array([1n, 2n, 3n]);
+    assertEq(BigInt64Array.prototype.with.length, 2);
+    assertDeepEq(bigIntArray.with(1, 9n), new BigInt64Array([1n, 9n, 3n]));
+    assertThrowsInstanceOf(() => bigIntArray.with(1, 9), TypeError);
+
+    var bigUintArray = new BigUint64Array([1n, 2n, 3n]);
+    assertDeepEq(bigUintArray.with(2, 4n), new BigUint64Array([1n, 2n, 4n]));
+    assertThrowsInstanceOf(() => bigUintArray.with(9, 1), TypeError);
+}
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
diff --git a/js/src/vm/TypedArrayObject.cpp b/js/src/vm/TypedArrayObject.cpp
index 068d4ef4a6..fd687946ca 100644
--- a/js/src/vm/TypedArrayObject.cpp
+++ b/js/src/vm/TypedArrayObject.cpp
@@ -1629,6 +1629,7 @@ TypedArrayObject::protoFunctions[] = {
     JS_SELF_HOSTED_FN("reduceRight", "TypedArrayReduceRight", 1, 0),
     JS_SELF_HOSTED_FN("reverse", "TypedArrayReverse", 0, 0),
     JS_SELF_HOSTED_FN("toReversed", "TypedArrayToReversed", 0, 0),
+    JS_SELF_HOSTED_FN("with", "TypedArrayWith", 2, 0),
     JS_SELF_HOSTED_FN("slice", "TypedArraySlice", 2, 0),
     JS_SELF_HOSTED_FN("some", "TypedArraySome", 1, 0),
     JS_SELF_HOSTED_FN("sort", "TypedArraySort", 1, 0),

From 2f2010005916befb241b2473107113bc656f044e Mon Sep 17 00:00:00 2001
From: Moonchild 
Date: Fri, 20 Mar 2026 13:26:18 +0100
Subject: [PATCH 4/6] Issue #3011 - Part 1: Add As{Text|Html}Editor() and
 AsEditorBase()

This adds helper functions to get specific types of content editors (if
possible) through the nsIEditor interface, as opposed to doing manual
casting and the ad hoc `GetHTMLEditor()` function.

Helper functions are spread out over multiple headers due to the
circular dependency issues that would be triggered otherwise.
---
 dom/base/nsINode.cpp                         |  1 +
 editor/libeditor/EditorBase.cpp              |  1 +
 editor/libeditor/EditorBase.h                | 20 ++++++++++
 editor/libeditor/HTMLEditRules.cpp           |  9 ++++-
 editor/libeditor/HTMLEditor.cpp              |  3 +-
 editor/libeditor/HTMLEditor.h                | 16 ++++++++
 editor/libeditor/HTMLEditorEventListener.cpp | 39 +++++++++++---------
 editor/libeditor/HTMLEditorEventListener.h   |  6 +--
 editor/libeditor/TextEditor.cpp              |  1 +
 editor/libeditor/TextEditor.h                | 14 +++++++
 editor/nsIEditor.idl                         | 38 +++++++++++++++++++
 11 files changed, 123 insertions(+), 25 deletions(-)

diff --git a/dom/base/nsINode.cpp b/dom/base/nsINode.cpp
index 5218938ca3..1766e9b920 100644
--- a/dom/base/nsINode.cpp
+++ b/dom/base/nsINode.cpp
@@ -16,6 +16,7 @@
 #include "mozilla/CORSMode.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozilla/EventListenerManager.h"
+#include "mozilla/HTMLEditor.h"
 #include "mozilla/InternalMutationEvent.h"
 #include "mozilla/Likely.h"
 #include "mozilla/MemoryReporting.h"
diff --git a/editor/libeditor/EditorBase.cpp b/editor/libeditor/EditorBase.cpp
index 60df3571ef..2a144e2738 100644
--- a/editor/libeditor/EditorBase.cpp
+++ b/editor/libeditor/EditorBase.cpp
@@ -142,6 +142,7 @@ EditorBase::EditorBase()
   , mDispatchInputEvent(true)
   , mIsInEditAction(false)
   , mHidingCaret(false)
+  , mIsHTMLEditorClass(false)
 {
 }
 
diff --git a/editor/libeditor/EditorBase.h b/editor/libeditor/EditorBase.h
index 08a895dcdc..2d1a60dba1 100644
--- a/editor/libeditor/EditorBase.h
+++ b/editor/libeditor/EditorBase.h
@@ -114,6 +114,7 @@ class DeleteNodeTransaction;
 class DeleteTextTransaction;
 class EditAggregateTransaction;
 class ErrorResult;
+class HTMLEditor;
 class InsertNodeTransaction;
 class InsertTextTransaction;
 class JoinNodeTransaction;
@@ -121,6 +122,7 @@ class PlaceholderTransaction;
 class RemoveStyleSheetTransaction;
 class SplitNodeTransaction;
 class TextComposition;
+class TextEditor;
 struct EditorDOMPoint;
 
 namespace dom {
@@ -1113,14 +1115,32 @@ protected:
   bool mIsInEditAction;
   // Whether caret is hidden forcibly.
   bool mHidingCaret;
+  // Whether we are an HTML editor class.
+  bool mIsHTMLEditorClass;
 
   friend bool NSCanUnload(nsISupports* serviceMgr);
   friend class AutoRules;
   friend class AutoSelectionRestorer;
   friend class AutoTransactionsConserveSelection;
   friend class RangeUpdater;
+  friend class nsIEditor;
 };
 
 } // namespace mozilla
 
+// nsIEditor helper functions.
+// Here because of code context.
+mozilla::EditorBase*
+nsIEditor::AsEditorBase()
+{
+  return static_cast(this);
+}
+
+const mozilla::EditorBase*
+nsIEditor::AsEditorBase() const
+{
+  return static_cast(this);
+}
+
+
 #endif // #ifndef mozilla_EditorBase_h
diff --git a/editor/libeditor/HTMLEditRules.cpp b/editor/libeditor/HTMLEditRules.cpp
index a5b284d82f..c1f90d4483 100644
--- a/editor/libeditor/HTMLEditRules.cpp
+++ b/editor/libeditor/HTMLEditRules.cpp
@@ -233,9 +233,16 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLEditRules, TextEditRules,
 NS_IMETHODIMP
 HTMLEditRules::Init(TextEditor* aTextEditor)
 {
+  if (NS_WARN_IF(!aTextEditor)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
   InitFields();
 
-  mHTMLEditor = static_cast(aTextEditor);
+  mHTMLEditor = aTextEditor->AsHTMLEditor();
+  if (NS_WARN_IF(!mHTMLEditor)) {
+    return NS_ERROR_INVALID_ARG;
+  }
 
   // call through to base class Init
   nsresult rv = TextEditRules::Init(aTextEditor);
diff --git a/editor/libeditor/HTMLEditor.cpp b/editor/libeditor/HTMLEditor.cpp
index 767856a1bc..1faccd1e15 100644
--- a/editor/libeditor/HTMLEditor.cpp
+++ b/editor/libeditor/HTMLEditor.cpp
@@ -132,6 +132,7 @@ HTMLEditor::HTMLEditor()
   , mPositionedObjectBorderTop(0)
   , mGridSize(0)
 {
+  mIsHTMLEditorClass = true;
 }
 
 HTMLEditor::~HTMLEditor()
@@ -501,7 +502,7 @@ HTMLEditor::InitRules()
     // instantiate the rules for the html editor
     mRules = new HTMLEditRules();
   }
-  return mRules->Init(static_cast(this));
+  return mRules->Init(this);
 }
 
 NS_IMETHODIMP
diff --git a/editor/libeditor/HTMLEditor.h b/editor/libeditor/HTMLEditor.h
index e90e7ebe6e..f314d24d00 100644
--- a/editor/libeditor/HTMLEditor.h
+++ b/editor/libeditor/HTMLEditor.h
@@ -1108,4 +1108,20 @@ private:
 
 } // namespace mozilla
 
+// nsIEditor helper functions.
+// Here because of code context.
+mozilla::HTMLEditor*
+nsIEditor::AsHTMLEditor()
+{
+  return static_cast(this)->mIsHTMLEditorClass ?
+           static_cast(this) : nullptr;
+}
+
+const mozilla::HTMLEditor*
+nsIEditor::AsHTMLEditor() const
+{
+  return static_cast(this)->mIsHTMLEditorClass ?
+           static_cast(this) : nullptr;
+}
+
 #endif // #ifndef mozilla_HTMLEditor_h
diff --git a/editor/libeditor/HTMLEditorEventListener.cpp b/editor/libeditor/HTMLEditorEventListener.cpp
index aa767519c7..3699e11acb 100644
--- a/editor/libeditor/HTMLEditorEventListener.cpp
+++ b/editor/libeditor/HTMLEditorEventListener.cpp
@@ -30,24 +30,18 @@ namespace mozilla {
 
 using namespace dom;
 
-#ifdef DEBUG
 nsresult
 HTMLEditorEventListener::Connect(EditorBase* aEditorBase)
 {
-  nsCOMPtr htmlEditor = do_QueryObject(aEditorBase);
-  nsCOMPtr htmlInlineTableEditor =
-    do_QueryObject(aEditorBase);
-  NS_PRECONDITION(htmlEditor && htmlInlineTableEditor,
-                  "Set HTMLEditor or its sub class");
-  return EditorEventListener::Connect(aEditorBase);
-}
-#endif
-
-HTMLEditor*
-HTMLEditorEventListener::GetHTMLEditor()
-{
-  // mEditor must be HTMLEditor or its subclass.
-  return static_cast(mEditorBase);
+  if (NS_WARN_IF(!aEditorBase)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+  // Guarantee that mEditorBase is always HTMLEditor.
+  HTMLEditor* htmlEditor = aEditorBase->AsHTMLEditor();
+  if (NS_WARN_IF(!htmlEditor)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+  return EditorEventListener::Connect(htmlEditor);
 }
 
 nsresult
@@ -59,7 +53,8 @@ HTMLEditorEventListener::MouseUp(nsIDOMMouseEvent* aMouseEvent)
 
   // FYI: We need to notify HTML editor of mouseup even if it's consumed
   //      because HTML editor always needs to release grabbing resizer.
-  HTMLEditor* htmlEditor = GetHTMLEditor();
+  HTMLEditor* htmlEditor = mEditorBase->AsHTMLEditor();
+  MOZ_ASSERT(htmlEditor);
 
   nsCOMPtr target;
   nsresult rv = aMouseEvent->AsEvent()->GetTarget(getter_AddRefs(target));
@@ -85,7 +80,9 @@ HTMLEditorEventListener::MouseDown(nsIDOMMouseEvent* aMouseEvent)
   WidgetMouseEvent* mousedownEvent =
     aMouseEvent->AsEvent()->WidgetEventPtr()->AsMouseEvent();
 
-  HTMLEditor* htmlEditor = GetHTMLEditor();
+  HTMLEditor* htmlEditor = mEditorBase->AsHTMLEditor();
+  MOZ_ASSERT(htmlEditor);
+
   // Contenteditable should disregard mousedowns outside it.
   // IsAcceptableInputEvent() checks it for a mouse event.
   if (!htmlEditor->IsAcceptableInputEvent(mousedownEvent)) {
@@ -221,13 +218,19 @@ HTMLEditorEventListener::MouseDown(nsIDOMMouseEvent* aMouseEvent)
 nsresult
 HTMLEditorEventListener::MouseClick(nsIDOMMouseEvent* aMouseEvent)
 {
+  if (NS_WARN_IF(DetachedFromEditor())) {
+    return NS_OK;
+  }
+
   nsCOMPtr target;
   nsresult rv = aMouseEvent->AsEvent()->GetTarget(getter_AddRefs(target));
   NS_ENSURE_SUCCESS(rv, rv);
   NS_ENSURE_TRUE(target, NS_ERROR_NULL_POINTER);
   nsCOMPtr element = do_QueryInterface(target);
 
-  GetHTMLEditor()->DoInlineTableEditingAction(element);
+  HTMLEditor* htmlEditor = mEditorBase->AsHTMLEditor();
+  MOZ_ASSERT(htmlEditor);
+  htmlEditor->DoInlineTableEditingAction(element);
 
   return EditorEventListener::MouseClick(aMouseEvent);
 }
diff --git a/editor/libeditor/HTMLEditorEventListener.h b/editor/libeditor/HTMLEditorEventListener.h
index b97b675b51..69bbf705d9 100644
--- a/editor/libeditor/HTMLEditorEventListener.h
+++ b/editor/libeditor/HTMLEditorEventListener.h
@@ -25,17 +25,13 @@ public:
   {
   }
 
-#ifdef DEBUG
-  // WARNING: You must be use HTMLEditor or its sub class for this class.
+  // Connect() fails if aEditorBase isn't an HTMLEditor instance.
   virtual nsresult Connect(EditorBase* aEditorBase) override;
-#endif
 
 protected:
   virtual nsresult MouseDown(nsIDOMMouseEvent* aMouseEvent) override;
   virtual nsresult MouseUp(nsIDOMMouseEvent* aMouseEvent) override;
   virtual nsresult MouseClick(nsIDOMMouseEvent* aMouseEvent) override;
-
-  inline HTMLEditor* GetHTMLEditor();
 };
 
 } // namespace mozilla
diff --git a/editor/libeditor/TextEditor.cpp b/editor/libeditor/TextEditor.cpp
index 5f75e7e7e0..2b56ae4fc4 100644
--- a/editor/libeditor/TextEditor.cpp
+++ b/editor/libeditor/TextEditor.cpp
@@ -10,6 +10,7 @@
 #include "gfxFontUtils.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/EditorUtils.h" // AutoEditBatch, AutoRules
+#include "mozilla/HTMLEditor.h"
 #include "mozilla/mozalloc.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/TextEditRules.h"
diff --git a/editor/libeditor/TextEditor.h b/editor/libeditor/TextEditor.h
index 7bb594931b..f4a744d759 100644
--- a/editor/libeditor/TextEditor.h
+++ b/editor/libeditor/TextEditor.h
@@ -247,4 +247,18 @@ protected:
 
 } // namespace mozilla
 
+// nsIEditor helper functions.
+// Here because of code context.
+mozilla::TextEditor*
+nsIEditor::AsTextEditor()
+{
+  return static_cast(this);
+}
+
+const mozilla::TextEditor*
+nsIEditor::AsTextEditor() const
+{
+  return static_cast(this);
+}
+
 #endif // #ifndef mozilla_TextEditor_h
diff --git a/editor/nsIEditor.idl b/editor/nsIEditor.idl
index bb9026d0ee..d96a3954e9 100644
--- a/editor/nsIEditor.idl
+++ b/editor/nsIEditor.idl
@@ -21,6 +21,14 @@ interface nsIEditActionListener;
 interface nsIInlineSpellChecker;
 interface nsITransferable;
 
+%{C++
+namespace mozilla {
+class EditorBase;
+class HTMLEditor;
+class TextEditor;
+} // namespace mozilla
+%}
+ 
 [scriptable, uuid(094be624-f0bf-400f-89e2-6a84baab9474)]
 interface nsIEditor  : nsISupports
 {
@@ -564,4 +572,34 @@ interface nsIEditor  : nsISupports
    * or nsIEditorObserver::CancelEditAction().  Otherwise, false.
    */
   [noscript] readonly attribute boolean isInEditAction;
+
+%{C++
+  /**
+   * AsEditorBase() returns a pointer to EditorBase class.
+   *
+   * In order to avoid circular dependency issues, this method is defined
+   * in mozilla/EditorBase.h.  Consumers need to #include that header.
+   */
+  inline mozilla::EditorBase* AsEditorBase();
+  inline const mozilla::EditorBase* AsEditorBase() const;
+
+  /**
+   * AsTextEditor() returns a pointer to TextEditor class.
+   *
+   * In order to avoid circular dependency issues, this method is defined
+   * in mozilla/TextEditor.h.  Consumers need to #include that header.
+   */
+  inline mozilla::TextEditor* AsTextEditor();
+  inline const mozilla::TextEditor* AsTextEditor() const;
+
+  /**
+   * AsHTMLEditor() returns a pointer to HTMLEditor class.
+   *
+   * In order to avoid circular dependency issues, this method is defined
+   * in mozilla/HTMLEditor.h.  Consumers need to #include that header.
+   */
+  inline mozilla::HTMLEditor* AsHTMLEditor();
+  inline const mozilla::HTMLEditor* AsHTMLEditor() const;
+%}
+
 };

From e701dad7ef2d67403ef88c9a85e5d680b0c1ce2b Mon Sep 17 00:00:00 2001
From: Moonchild 
Date: Fri, 20 Mar 2026 14:45:11 +0100
Subject: [PATCH 5/6] Issue #3011 - Part 2: Switch spellchecker root to Shadow
 DOM.

Set the root for spelling checker to shadow root if the contenteditable
nodes are in the shadow DOM. Bail if the position can't be set properly.
---
 .../spellcheck/src/mozInlineSpellChecker.cpp  |  7 ++-
 .../spellcheck/src/mozInlineSpellWordUtil.cpp | 54 ++++++++++++-------
 .../spellcheck/src/mozInlineSpellWordUtil.h   | 35 ++++++------
 3 files changed, 60 insertions(+), 36 deletions(-)

diff --git a/extensions/spellcheck/src/mozInlineSpellChecker.cpp b/extensions/spellcheck/src/mozInlineSpellChecker.cpp
index 398059fc9b..a50d390037 100644
--- a/extensions/spellcheck/src/mozInlineSpellChecker.cpp
+++ b/extensions/spellcheck/src/mozInlineSpellChecker.cpp
@@ -1490,8 +1490,11 @@ nsresult mozInlineSpellChecker::DoSpellCheck(mozInlineSpellWordUtil& aWordUtil,
       return NS_OK;
     }
 
-    aWordUtil.SetEnd(endNode, endOffset);
-    aWordUtil.SetPosition(beginNode, beginOffset);
+    nsresult rv = aWordUtil.SetPositionAndEnd(beginNode, beginOffset, endNode, endOffset);
+    if (NS_FAILED(rv)) {
+      // Just bail out and don't try to spell-check this
+      return NS_OK;
+    }
   }
 
   // aWordUtil.SetPosition flushes pending notifications, check editor again.
diff --git a/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp b/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp
index 460ac46b85..13e621c870 100644
--- a/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp
+++ b/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp
@@ -23,6 +23,8 @@
 #include "nsIFrame.h"
 #include 
 #include "mozilla/BinarySearch.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/dom/ShadowRoot.h"
 
 using namespace mozilla;
 
@@ -69,8 +71,10 @@ mozInlineSpellWordUtil::Init(nsWeakPtr aWeakEditor)
   mDOMDocument = domDoc;
   mDocument = do_QueryInterface(domDoc);
 
-  // Find the root node for the editor. For contenteditable we'll need something
-  // cleverer here.
+  mIsContentEditableOrDesignMode = !!editor->AsHTMLEditor();
+
+  // Find the root node for the editor. For contenteditable the mRootNode could
+  // change to shadow root if the begin and end are inside the shadowDOM.
   nsCOMPtr rootElt;
   rv = editor->GetRootElement(getter_AddRefs(rootElt));
   NS_ENSURE_SUCCESS(rv, rv);
@@ -154,7 +158,7 @@ FindNextTextNode(nsINode* aNode, int32_t aOffset, nsINode* aRoot)
   return checkNode;
 }
 
-// mozInlineSpellWordUtil::SetEnd
+// mozInlineSpellWordUtil::SetPositionAndEnd
 //
 //    We have two ranges "hard" and "soft". The hard boundary is simply
 //    the scope of the root node. The soft boundary is that which is set
@@ -172,34 +176,44 @@ FindNextTextNode(nsINode* aNode, int32_t aOffset, nsINode* aRoot)
 //    position.
 
 nsresult
-mozInlineSpellWordUtil::SetEnd(nsINode* aEndNode, int32_t aEndOffset)
+mozInlineSpellWordUtil::SetPositionAndEnd(nsINode* aPositionNode,
+                                          int32_t aPositionOffset,
+                                          nsINode* aEndNode,
+                                          int32_t aEndOffset)
 {
+  MOZ_ASSERT(aPositionNode, "Null begin node?");
   NS_PRECONDITION(aEndNode, "Null end node?");
 
   NS_ASSERTION(mRootNode, "Not initialized");
 
+  // Find a appropriate root if we are dealing with contenteditable nodes which
+  // are in the shadow DOM. See UXP Issue #3011
+  if (mIsContentEditableOrDesignMode) {
+    nsINode* rootNode = aPositionNode->SubtreeRoot();
+    if (rootNode != aEndNode->SubtreeRoot()) {
+      return NS_ERROR_FAILURE;
+    }
+
+    if (mozilla::dom::ShadowRoot::FromNode(rootNode)) {
+      mRootNode = rootNode;
+    }
+  }
+
   InvalidateWords();
 
+  if (!IsTextNode(aPositionNode)) {
+    // Start at the start of the first text node after aNode/aOffset.
+    aPositionNode = FindNextTextNode(aPositionNode, aPositionOffset, mRootNode);
+    aPositionOffset = 0;
+  }
+  mSoftBegin = NodeOffset(aPositionNode, aPositionOffset);
+
   if (!IsTextNode(aEndNode)) {
     // End at the start of the first text node after aEndNode/aEndOffset.
     aEndNode = FindNextTextNode(aEndNode, aEndOffset, mRootNode);
     aEndOffset = 0;
   }
   mSoftEnd = NodeOffset(aEndNode, aEndOffset);
-  return NS_OK;
-}
-
-nsresult
-mozInlineSpellWordUtil::SetPosition(nsINode* aNode, int32_t aOffset)
-{
-  InvalidateWords();
-
-  if (!IsTextNode(aNode)) {
-    // Start at the start of the first text node after aNode/aOffset.
-    aNode = FindNextTextNode(aNode, aOffset, mRootNode);
-    aOffset = 0;
-  }
-  mSoftBegin = NodeOffset(aNode, aOffset);
 
   nsresult rv = EnsureWords();
   if (NS_FAILED(rv)) {
@@ -207,8 +221,10 @@ mozInlineSpellWordUtil::SetPosition(nsINode* aNode, int32_t aOffset)
   }
   
   int32_t textOffset = MapDOMPositionToSoftTextOffset(mSoftBegin);
-  if (textOffset < 0)
+  if (textOffset < 0) {
     return NS_OK;
+  }
+
   mNextWordIndex = FindRealWordContaining(textOffset, HINT_END, true);
   return NS_OK;
 }
diff --git a/extensions/spellcheck/src/mozInlineSpellWordUtil.h b/extensions/spellcheck/src/mozInlineSpellWordUtil.h
index b28d24ae5f..213fa52a16 100644
--- a/extensions/spellcheck/src/mozInlineSpellWordUtil.h
+++ b/extensions/spellcheck/src/mozInlineSpellWordUtil.h
@@ -29,12 +29,11 @@ class nsINode;
  *    The basic operation is:
  *
  *    1. Call Init with the weak pointer to the editor that you're using.
- *    2. Call SetEnd to set where you want to stop spellchecking. We'll stop
- *       at the word boundary after that. If SetEnd is not called, we'll stop
- *       at the end of the document's root element.
- *    3. Call SetPosition to initialize the current position inside the
- *       previously given range.
- *    4. Call GetNextWord over and over until it returns false.
+ *    2. Call SetPositionAndEnd to to initialize the current position inside the
+ *       previously given range and set where you want to stop spellchecking. 
+ *       We'll stop at the word boundary after that. If SetEnd is not called,
+ *       we'll stop at the end of the root element.
+ *    3. Call GetNextWord over and over until it returns false.
  */
 
 class mozInlineSpellWordUtil
@@ -57,17 +56,22 @@ public:
   };
 
   mozInlineSpellWordUtil()
-    : mRootNode(nullptr),
-      mSoftBegin(nullptr, 0), mSoftEnd(nullptr, 0),
-      mNextWordIndex(-1), mSoftTextValid(false) {}
+    : mIsContentEditableOrDesignMode(false)
+    , mRootNode(nullptr)
+    , mSoftBegin(nullptr, 0)
+    , mSoftEnd(nullptr, 0)
+    , mNextWordIndex(-1)
+    , mSoftTextValid(false)
+  {}
 
   nsresult Init(nsWeakPtr aWeakEditor);
 
-  nsresult SetEnd(nsINode* aEndNode, int32_t aEndOffset);
-
-  // sets the current position, this should be inside the range. If we are in
-  // the middle of a word, we'll move to its start.
-  nsresult SetPosition(nsINode* aNode, int32_t aOffset);
+  // Sets the current position and end. This should be inside the range.
+  // If we are in the middle of a word, we'll move to its start.
+  nsresult SetPositionAndEnd(nsINode* aPositionNode,
+                             int32_t aPositionOffset,
+                             nsINode* aEndNode,
+                             int32_t aEndOffset);
 
   // Given a point inside or immediately following a word, this returns the
   // DOM range that exactly encloses that word's characters. The current
@@ -100,7 +104,8 @@ private:
 
   // cached stuff for the editor, set by Init
   nsCOMPtr mDOMDocument;
-  nsCOMPtr         mDocument;
+  nsCOMPtr mDocument;
+  bool mIsContentEditableOrDesignMode;
 
   // range to check, see SetPosition and SetEnd
   nsINode*    mRootNode;

From 964a72079a66bc70c2e74adc76ed71a9f83e71ec Mon Sep 17 00:00:00 2001
From: Moonchild 
Date: Fri, 20 Mar 2026 17:07:55 +0100
Subject: [PATCH 6/6] Issue #3011 - Part 3: Handle edge case for spellchecking.

Make spellchecking work in Shadow DOM after anchor navigates away from it.
---
 .../spellcheck/src/mozInlineSpellChecker.cpp  | 23 +------------------
 1 file changed, 1 insertion(+), 22 deletions(-)

diff --git a/extensions/spellcheck/src/mozInlineSpellChecker.cpp b/extensions/spellcheck/src/mozInlineSpellChecker.cpp
index a50d390037..f62c1852e0 100644
--- a/extensions/spellcheck/src/mozInlineSpellChecker.cpp
+++ b/extensions/spellcheck/src/mozInlineSpellChecker.cpp
@@ -97,9 +97,6 @@ using namespace mozilla::dom;
 #define INLINESPELL_STARTED_TOPIC "inlineSpellChecker-spellCheck-started"
 #define INLINESPELL_ENDED_TOPIC "inlineSpellChecker-spellCheck-ended"
 
-static bool ContentIsDescendantOf(nsINode* aPossibleDescendant,
-                                    nsINode* aPossibleAncestor);
-
 static const char kMaxSpellCheckSelectionSize[] = "extensions.spellcheck.inline.max-misspellings";
 
 mozInlineSpellStatus::mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker)
@@ -238,7 +235,7 @@ mozInlineSpellStatus::InitForNavigation(
   NS_ENSURE_SUCCESS(rv, rv);
   nsCOMPtr currentAnchor = do_QueryInterface(aOldAnchorNode, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
-  if (root && currentAnchor && ! ContentIsDescendantOf(currentAnchor, root)) {
+  if (root && currentAnchor && !nsContentUtils::ContentIsShadowIncludingDescendantOf(currentAnchor, root)) {
     *aContinue = false;
     return NS_OK;
   }
@@ -1843,24 +1840,6 @@ nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition()
   return NS_OK;
 }
 
-// This is a copy of nsContentUtils::ContentIsDescendantOf. Another crime
-// for XPCOM's rap sheet
-bool // static
-ContentIsDescendantOf(nsINode* aPossibleDescendant,
-                      nsINode* aPossibleAncestor)
-{
-  NS_PRECONDITION(aPossibleDescendant, "The possible descendant is null!");
-  NS_PRECONDITION(aPossibleAncestor, "The possible ancestor is null!");
-
-  do {
-    if (aPossibleDescendant == aPossibleAncestor)
-      return true;
-    aPossibleDescendant = aPossibleDescendant->GetParentNode();
-  } while (aPossibleDescendant);
-
-  return false;
-}
-
 // mozInlineSpellChecker::HandleNavigationEvent
 //
 //    Acts upon mouse clicks and keyboard navigation changes, spell checking