diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..9b58c4f
Binary files /dev/null and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
index 7d690dd..70b1d5c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,4 +44,5 @@ private
closedsrc
.venv
-bundle
\ No newline at end of file
+bundle
+*.pem
\ No newline at end of file
diff --git a/Makefile b/Makefile
index fcb3717..a7efbde 100644
--- a/Makefile
+++ b/Makefile
@@ -11,7 +11,7 @@ pacman := python python-pip p7zip go msitools wget aria2
.PHONY: help fetch setup setup-minimal clean set-target distclean build package \
build-launcher check-arch revert edits run bootstrap mozbootstrap dir \
package-linux package-macos package-windows vcredist_arch patch unpatch \
- workspace check-arg edit-cfg ff-dbg tests update-ubo-assets
+ workspace check-arg edit-cfg ff-dbg tests update-ubo-assets generate-assets-car
help:
@echo "Available targets:"
@@ -260,6 +260,9 @@ path:
update-ubo-assets:
bash ./scripts/update-ubo-assets.sh
+generate-assets-car:
+ bash ./scripts/generate-assets-car.sh
+
upload:
# ===============================
# This is only for internal use. You can ignore this.
diff --git a/README.md b/README.md
index ed49a8d..084612c 100644
--- a/README.md
+++ b/README.md
@@ -20,8 +20,13 @@ Camoufox is an open source anti-detect browser for robust fingerprint injection
Camoufox is the most modern, effective & future-proof open source solution for avoiding bot detection and intelligent fingerprint rotation. It outperforms most commercial anti-bot browsers.
+## Firefox Version Upgrade Notice
+The current main branch is built for Firefox v146. It is an experimental change and may contain several bugs. If you are building from source and require a stable production version, use branch `releases/135`.
+
---
+## Sponsors
+
@@ -30,6 +35,14 @@ Camoufox is the most modern, effective & future-proof open source solution for a
---
+
+
+
+
+
+[Clover Labs](https://cloverlabs.ai/?utm_source=github&utm_medium=sponsoring&utm_campaign=camoufox) is a Toronto based venture studio building AI agents for growth and distribution.
+
+
## Features
- Invisible to **all anti-bot systems** 🎭
diff --git a/additions/browser/branding/camoufox/Assets.car b/additions/browser/branding/camoufox/Assets.car
new file mode 100644
index 0000000..4e16ada
Binary files /dev/null and b/additions/browser/branding/camoufox/Assets.car differ
diff --git a/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/Contents.json b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..64dc11e
--- /dev/null
+++ b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_16x16.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "16x16"
+ },
+ {
+ "filename" : "icon_16x16@2x.png",
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "16x16"
+ },
+ {
+ "filename" : "icon_32x32.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "32x32"
+ },
+ {
+ "filename" : "icon_32x32@2x.png",
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "32x32"
+ },
+ {
+ "filename" : "icon_128x128.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "128x128"
+ },
+ {
+ "filename" : "icon_128x128@2x.png",
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "128x128"
+ },
+ {
+ "filename" : "icon_256x256.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "256x256"
+ },
+ {
+ "filename" : "icon_256x256@2x.png",
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "256x256"
+ },
+ {
+ "filename" : "icon_512x512.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "512x512"
+ },
+ {
+ "filename" : "icon_512x512@2x.png",
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "512x512"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
new file mode 100644
index 0000000..703bf01
Binary files /dev/null and b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ
diff --git a/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
new file mode 100644
index 0000000..58d2f0d
Binary files /dev/null and b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ
diff --git a/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
new file mode 100644
index 0000000..9ee67be
Binary files /dev/null and b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ
diff --git a/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
new file mode 100644
index 0000000..ce71b6c
Binary files /dev/null and b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ
diff --git a/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
new file mode 100644
index 0000000..58d2f0d
Binary files /dev/null and b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ
diff --git a/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
new file mode 100644
index 0000000..2e11d2c
Binary files /dev/null and b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ
diff --git a/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
new file mode 100644
index 0000000..ce71b6c
Binary files /dev/null and b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ
diff --git a/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
new file mode 100644
index 0000000..303b34e
Binary files /dev/null and b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ
diff --git a/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
new file mode 100644
index 0000000..2e11d2c
Binary files /dev/null and b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ
diff --git a/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
new file mode 100644
index 0000000..4e3ea7f
Binary files /dev/null and b/additions/browser/branding/camoufox/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ
diff --git a/additions/browser/branding/camoufox/Assets.xcassets/Contents.json b/additions/browser/branding/camoufox/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/additions/browser/branding/camoufox/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/additions/browser/branding/camoufox/configure.sh b/additions/browser/branding/camoufox/configure.sh
index 73517f1..98eb6e1 100644
--- a/additions/browser/branding/camoufox/configure.sh
+++ b/additions/browser/branding/camoufox/configure.sh
@@ -2,12 +2,10 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-# there is a possible patch to consider when changing this:
-# see: patches/browser-confvars.patch
+# Note: MOZ_APP_VENDOR and MOZ_APP_PROFILE must be set via imply_option() in browser/moz.configure
+# See patches/librewolf/disable-data-reporting-at-compile-time.patch
MOZ_APP_NAME=camoufox
MOZ_APP_BASENAME=Camoufox
-MOZ_APP_PROFILE=camoufox
-MOZ_APP_VENDOR=Camoufox
MOZ_APP_DISPLAYNAME=Camoufox
MOZ_APP_REMOTINGNAME=camoufox
diff --git a/additions/juggler/ChannelEventSink.sys.mjs b/additions/juggler/ChannelEventSink.sys.mjs
new file mode 100644
index 0000000..ab03e9f
--- /dev/null
+++ b/additions/juggler/ChannelEventSink.sys.mjs
@@ -0,0 +1,99 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { ComponentUtils } from "resource://gre/modules/ComponentUtils.sys.mjs";
+
+const Cm = Components.manager;
+
+/**
+ * This is a nsIChannelEventSink implementation that monitors channel redirects.
+ * This has been forked from:
+ * https://searchfox.org/mozilla-central/source/devtools/server/actors/network-monitor/channel-event-sink.js
+ * The rest of this module is also more or less forking:
+ * https://searchfox.org/mozilla-central/source/devtools/server/actors/network-monitor/network-observer.js
+ * TODO(try to re-unify /remote/ with /devtools code)
+ */
+const SINK_CLASS_DESCRIPTION = "NetworkMonitor Channel Event Sink";
+const SINK_CLASS_ID = Components.ID("{c2b4c83e-607a-405a-beab-0ef5dbfb7617}");
+const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1";
+const SINK_CATEGORY_NAME = "net-channel-event-sinks";
+
+function ChannelEventSink() {
+ this.wrappedJSObject = this;
+ this.collectors = new Set();
+}
+
+ChannelEventSink.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIChannelEventSink"]),
+
+ registerCollector(collector) {
+ this.collectors.add(collector);
+ },
+
+ unregisterCollector(collector) {
+ this.collectors.delete(collector);
+
+ if (this.collectors.size == 0) {
+ ChannelEventSinkFactory.unregister();
+ }
+ },
+
+ asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+ for (const collector of this.collectors) {
+ try {
+ collector._onChannelRedirect(oldChannel, newChannel, flags);
+ } catch (ex) {
+ console.error(
+ "StackTraceCollector.onChannelRedirect threw an exception",
+ ex
+ );
+ }
+ }
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ },
+};
+
+export const ChannelEventSinkFactory =
+ ComponentUtils.generateSingletonFactory(ChannelEventSink);
+
+ChannelEventSinkFactory.register = function () {
+ const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ if (registrar.isCIDRegistered(SINK_CLASS_ID)) {
+ return;
+ }
+
+ registrar.registerFactory(
+ SINK_CLASS_ID,
+ SINK_CLASS_DESCRIPTION,
+ SINK_CONTRACT_ID,
+ ChannelEventSinkFactory
+ );
+
+ Services.catMan.addCategoryEntry(
+ SINK_CATEGORY_NAME,
+ SINK_CONTRACT_ID,
+ SINK_CONTRACT_ID,
+ false,
+ true
+ );
+};
+
+ChannelEventSinkFactory.unregister = function () {
+ const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.unregisterFactory(SINK_CLASS_ID, ChannelEventSinkFactory);
+
+ Services.catMan.deleteCategoryEntry(
+ SINK_CATEGORY_NAME,
+ SINK_CONTRACT_ID,
+ false
+ );
+};
+
+ChannelEventSinkFactory.getService = function () {
+ // Make sure the ChannelEventSink service is registered before accessing it
+ ChannelEventSinkFactory.register();
+
+ return Cc[SINK_CONTRACT_ID].getService(Ci.nsIChannelEventSink)
+ .wrappedJSObject;
+};
diff --git a/additions/juggler/Helper.js b/additions/juggler/Helper.js
index f5a64d6..875dd5a 100644
--- a/additions/juggler/Helper.js
+++ b/additions/juggler/Helper.js
@@ -4,9 +4,9 @@
const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
-class Helper {
+export class Helper {
decorateAsEventEmitter(objectToDecorate) {
- const { EventEmitter } = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
+ const { EventEmitter } = ChromeUtils.importESModule('resource://gre/modules/EventEmitter.sys.mjs');
const emitter = new EventEmitter();
objectToDecorate.on = emitter.on.bind(emitter);
objectToDecorate.addEventListener = emitter.on.bind(emitter);
@@ -172,7 +172,7 @@ class Helper {
const helper = new Helper();
-class EventWatcher {
+export class EventWatcher {
constructor(receiver, eventNames, pendingEventWatchers = new Set()) {
this._pendingEventWatchers = pendingEventWatchers;
this._pendingEventWatchers.add(this);
@@ -233,7 +233,3 @@ class EventWatcher {
}
}
-var EXPORTED_SYMBOLS = [ "Helper", "EventWatcher" ];
-this.Helper = Helper;
-this.EventWatcher = EventWatcher;
-
diff --git a/additions/juggler/JugglerFrameParent.jsm b/additions/juggler/JugglerFrameParent.jsm
index e621fab..9d41842 100644
--- a/additions/juggler/JugglerFrameParent.jsm
+++ b/additions/juggler/JugglerFrameParent.jsm
@@ -1,13 +1,11 @@
"use strict";
-const { TargetRegistry } = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js');
-const { Helper } = ChromeUtils.import('chrome://juggler/content/Helper.js');
+const { TargetRegistry } = ChromeUtils.importESModule('chrome://juggler/content/TargetRegistry.js');
+const { Helper } = ChromeUtils.importESModule('chrome://juggler/content/Helper.js');
const helper = new Helper();
-var EXPORTED_SYMBOLS = ['JugglerFrameParent'];
-
-class JugglerFrameParent extends JSWindowActorParent {
+export class JugglerFrameParent extends JSWindowActorParent {
constructor() {
super();
}
@@ -18,7 +16,7 @@ class JugglerFrameParent extends JSWindowActorParent {
// Actors are registered per the WindowGlobalParent / WindowGlobalChild pair. We are only
// interested in those WindowGlobalParent actors that are matching current browsingContext
// window global.
- // See https://github.com/mozilla/gecko-dev/blob/cd2121e7d83af1b421c95e8c923db70e692dab5f/testing/mochitest/BrowserTestUtils/BrowserTestUtilsParent.sys.mjs#L15
+ // See https://github.com/mozilla-firefox/firefox/blob/35e22180b0b61413dd8eccf6c00b1c6fac073eee/testing/mochitest/BrowserTestUtils/BrowserTestUtilsParent.sys.mjs#L15
if (!this.manager?.isCurrentGlobal)
return;
diff --git a/additions/juggler/JugglerFrameParent.sys.mjs b/additions/juggler/JugglerFrameParent.sys.mjs
new file mode 100644
index 0000000..9c3b588
--- /dev/null
+++ b/additions/juggler/JugglerFrameParent.sys.mjs
@@ -0,0 +1,40 @@
+"use strict";
+
+const { TargetRegistry } = ChromeUtils.importESModule('chrome://juggler/content/TargetRegistry.js');
+const { Helper } = ChromeUtils.importESModule('chrome://juggler/content/Helper.js');
+
+const helper = new Helper();
+
+export class JugglerFrameParent extends JSWindowActorParent {
+ constructor() {
+ super();
+ }
+
+ receiveMessage() { }
+
+ async actorCreated() {
+ // Actors are registered per the WindowGlobalParent / WindowGlobalChild pair. We are only
+ // interested in those WindowGlobalParent actors that are matching current browsingContext
+ // window global.
+ // See https://github.com/mozilla/gecko-dev/blob/cd2121e7d83af1b421c95e8c923db70e692dab5f/testing/mochitest/BrowserTestUtils/BrowserTestUtilsParent.sys.mjs#L15
+ if (!this.manager?.isCurrentGlobal)
+ return;
+
+ // Only interested in main frames for now.
+ if (this.browsingContext.parent)
+ return;
+
+ this._target = TargetRegistry.instance()?.targetForBrowserId(this.browsingContext.browserId);
+ if (!this._target)
+ return;
+
+ this.actorName = `browser::page[${this._target.id()}]/${this.browsingContext.browserId}/${this.browsingContext.id}/${this._target.nextActorSequenceNumber()}`;
+ this._target.setActor(this);
+ }
+
+ didDestroy() {
+ if (!this._target)
+ return;
+ this._target.removeActor(this);
+ }
+}
diff --git a/additions/juggler/NetworkObserver.js b/additions/juggler/NetworkObserver.js
index ef269b0..2719cc1 100644
--- a/additions/juggler/NetworkObserver.js
+++ b/additions/juggler/NetworkObserver.js
@@ -4,9 +4,9 @@
"use strict";
-const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
-const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
-const { ChannelEventSinkFactory } = ChromeUtils.import("chrome://remote/content/cdp/observers/ChannelEventSink.jsm");
+const {Helper} = ChromeUtils.importESModule('chrome://juggler/content/Helper.js');
+const {NetUtil} = ChromeUtils.importESModule('resource://gre/modules/NetUtil.sys.mjs');
+const { ChannelEventSinkFactory } = ChromeUtils.importESModule("chrome://juggler/content/ChannelEventSink.sys.mjs");
const Cc = Components.classes;
@@ -28,7 +28,7 @@ const MAX_RESPONSE_STORAGE_SIZE = 100 * 1024 * 1024;
const pageNetworkSymbol = Symbol('PageNetwork');
-class PageNetwork {
+export class PageNetwork {
static forPageTarget(target) {
if (!target)
return undefined;
@@ -143,9 +143,10 @@ class NetworkRequest {
const target = this._networkObserver._targetRegistry.targetForBrowserId(browsingContext.browserId);
this._pageNetwork = PageNetwork.forPageTarget(target);
}
- this._expectingInterception = false;
+ this._shouldYieldInterceptionToServiceWorker = false;
this._expectingResumedRequest = undefined; // { method, headers, postData }
this._overriddenHeadersForRedirect = redirectedFrom?._overriddenHeadersForRedirect;
+ this._sentOnRequest = false;
this._sentOnResponse = false;
this._fulfilled = false;
@@ -204,11 +205,13 @@ class NetworkRequest {
this._interceptedChannel.synthesizeHeader(header.name, header.value);
if (header.name.toLowerCase() === 'set-cookie') {
Services.cookies.QueryInterface(Ci.nsICookieService);
- Services.cookies.setCookieStringFromHttp(this.httpChannel.URI, header.value, this.httpChannel);
+ for (const cookieString of header.value.split('\n'))
+ Services.cookies.setCookieStringFromHttp(this.httpChannel.URI, cookieString, this.httpChannel);
}
}
const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
- synthesized.data = base64body ? atob(base64body) : '';
+ if (base64body)
+ synthesized.setByteStringData(atob(base64body));
this._interceptedChannel.startSynthesizedResponse(synthesized, null, null, '', false);
this._interceptedChannel.finishSynthesizedResponse();
this._interceptedChannel = undefined;
@@ -316,9 +319,8 @@ class NetworkRequest {
const interceptController = this._fallThroughInterceptController();
if (interceptController && interceptController.shouldPrepareForIntercept(aURI, channel)) {
// We assume that interceptController is a service worker if there is one,
- // and yield interception to it. We are not going to intercept ourselves,
- // so we send onRequest now.
- this._sendOnRequest(false);
+ // and yield interception to it.
+ this._shouldYieldInterceptionToServiceWorker = true;
return true;
}
@@ -327,12 +329,6 @@ class NetworkRequest {
return false;
}
- // We do not want to intercept any redirects, because we are not able
- // to intercept subresource redirects, and it's unreliable for main requests.
- // We do not sendOnRequest here, because redirects do that in constructor.
- if (this.redirectedFromId)
- return false;
-
const shouldIntercept = this._shouldIntercept();
if (!shouldIntercept) {
// We are not intercepting - ready to issue onRequest.
@@ -340,21 +336,24 @@ class NetworkRequest {
return false;
}
- this._expectingInterception = true;
return true;
}
// nsINetworkInterceptController
channelIntercepted(intercepted) {
- if (!this._expectingInterception) {
- // We are not intercepting, fall-through.
- const interceptController = this._fallThroughInterceptController();
- if (interceptController)
- interceptController.channelIntercepted(intercepted);
+ // Yield to a service worker if determined so in shouldPrepareForIntercept().
+ const serviceWorker = this._shouldYieldInterceptionToServiceWorker ? this._fallThroughInterceptController() : undefined;
+ // Clear the flag to avoid an infinite loop. After service worker, we should intercept ourselves.
+ this._shouldYieldInterceptionToServiceWorker = false;
+
+ if (serviceWorker) {
+ const interceptedChannel = intercepted.QueryInterface(Ci.nsIInterceptedChannel);
+ // If service worker will not actually intercept the request, we want to be called again.
+ interceptedChannel.interceptAfterServiceWorkerResets();
+ serviceWorker.channelIntercepted(intercepted);
return;
}
- this._expectingInterception = false;
this._interceptedChannel = intercepted.QueryInterface(Ci.nsIInterceptedChannel);
const pageNetwork = this._pageNetwork;
@@ -408,6 +407,7 @@ class NetworkRequest {
// See https://github.com/microsoft/playwright/issues/9418#issuecomment-944836244
if (aRequest !== this.httpChannel)
return;
+ this._sendOnRequest(false);
try {
this._originalListener.onStartRequest(aRequest);
} catch (e) {
@@ -445,6 +445,10 @@ class NetworkRequest {
}
_shouldIntercept() {
+ // We do not want to intercept any redirects, because we are not able
+ // to intercept subresource redirects, and it's unreliable for main requests.
+ if (this.redirectedFromId)
+ return false;
const pageNetwork = this._pageNetwork;
if (!pageNetwork)
return false;
@@ -465,8 +469,15 @@ class NetworkRequest {
}
_sendOnRequest(isIntercepted) {
- // Note: we call _sendOnRequest either after we intercepted the request,
- // or at the first moment we know that we are not going to intercept.
+ if (this._sentOnRequest) {
+ // We can come here twice because:
+ // - Redirects call _sendOnRequest in the constructor and from inside interception.
+ // - All other requests might call _sendOnRequest from onStartRequest and from inside interception.
+ // - All requests call _sendOnRequest from _sendOnResponse to avoid responses without requests.
+ return;
+ }
+ this._sentOnRequest = true;
+
const pageNetwork = this._pageNetwork;
if (!pageNetwork)
return;
@@ -489,11 +500,21 @@ class NetworkRequest {
}
_sendOnResponse(fromCache, opt_statusCode, opt_statusText) {
+ // For internal redirects, and perhaps something else that we lack test coverage for,
+ // we can arrive here before onStartRequest has fired. Make sure we
+ // notify about the request first.
+ this._sendOnRequest(false);
+
if (this._sentOnResponse) {
- // We can come here twice because of internal redirects, e.g. service workers.
+ // We can come here twice because of an internal redirect, for example:
+ // - request was intercepted by a service worker;
+ // - HSTS redirect;
+ // - CORS preflight;
+ // - who knows what else?
return;
}
this._sentOnResponse = true;
+
const pageNetwork = this._pageNetwork;
if (!pageNetwork)
return;
@@ -512,6 +533,8 @@ class NetworkRequest {
};
const { status, statusText, headers } = responseHead(this.httpChannel, opt_statusCode, opt_statusText);
+ if (redirectStatus.includes(status) && this._overriddenHeadersForRedirect)
+ this._overriddenHeadersForRedirect = filterHeadersForRedirect(this._overriddenHeadersForRedirect, this.httpChannel.requestMethod, status);
let remoteIPAddress = undefined;
let remotePort = undefined;
try {
@@ -574,7 +597,7 @@ class NetworkRequest {
}
}
-class NetworkObserver {
+export class NetworkObserver {
static instance() {
return NetworkObserver._instance || null;
}
@@ -774,6 +797,10 @@ function clearRequestHeaders(httpChannel) {
// We cannot remove the "host" header.
if (header.name.toLowerCase() === 'host')
continue;
+ // Keep the "cookie" header. If there is an override, it will be set anyway.
+ // Otherwise, we may delete a cookie that was set for a redirect.
+ if (header.name.toLowerCase() === 'cookie')
+ continue;
httpChannel.setRequestHeader(header.name, '', false /* merge */);
}
}
@@ -783,6 +810,18 @@ function overrideRequestHeaders(httpChannel, headers) {
appendExtraHTTPHeaders(httpChannel, headers);
}
+const redirectStatus = [301, 302, 303, 307, 308];
+
+function filterHeadersForRedirect(headers, requestMethod, status) {
+ // HTTP-redirect fetch step 13 (https://fetch.spec.whatwg.org/#http-redirect-fetch)
+ if ((status === 301 || status === 302) && requestMethod === 'POST' ||
+ status === 303 && !['GET', 'HEAD'].includes(requestMethod)) {
+ const requestBodyHeaders = ['content-encoding', 'content-language', 'content-length', 'content-location', 'content-type'];
+ return headers.filter(header => !requestBodyHeaders.includes(header.name.toLowerCase()));
+ }
+ return headers;
+}
+
function causeTypeToString(causeType) {
for (let key in Ci.nsIContentPolicy) {
if (Ci.nsIContentPolicy[key] === causeType)
@@ -870,7 +909,7 @@ function setPostData(httpChannel, postData, headers) {
return;
const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
const body = atob(postData);
- synthesized.setByteStringData(body, body.length);
+ synthesized.setByteStringData(body);
const overriddenHeader = (lowerCaseName) => {
if (headers) {
@@ -902,7 +941,7 @@ function convertString(s, source, dest) {
const is = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
Ci.nsIStringInputStream
);
- is.setByteStringData(s, s.length);
+ is.setByteStringData(s);
const listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
Ci.nsIStreamLoader
);
@@ -962,6 +1001,3 @@ PageNetwork.Events = {
RequestFailed: Symbol('PageNetwork.Events.RequestFailed'),
};
-var EXPORTED_SYMBOLS = ['NetworkObserver', 'PageNetwork'];
-this.NetworkObserver = NetworkObserver;
-this.PageNetwork = PageNetwork;
\ No newline at end of file
diff --git a/additions/juggler/SimpleChannel.js b/additions/juggler/SimpleChannel.js
index a4e1b19..cc55545 100644
--- a/additions/juggler/SimpleChannel.js
+++ b/additions/juggler/SimpleChannel.js
@@ -79,7 +79,7 @@ class SimpleChannel {
_setTimeout(cb, timeout) {
// Lazy load on first call.
- this._setTimeout = ChromeUtils.import('resource://gre/modules/Timer.jsm').setTimeout;
+ this._setTimeout = ChromeUtils.importESModule('resource://gre/modules/Timer.sys.mjs').setTimeout;
this._setTimeout(cb, timeout);
}
@@ -251,6 +251,3 @@ class SimpleChannel {
}
}
}
-
-var EXPORTED_SYMBOLS = ['SimpleChannel'];
-this.SimpleChannel = SimpleChannel;
diff --git a/additions/juggler/TargetRegistry.js b/additions/juggler/TargetRegistry.js
index 4a73580..55afdad 100644
--- a/additions/juggler/TargetRegistry.js
+++ b/additions/juggler/TargetRegistry.js
@@ -1,19 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
-* License, v. 2.0. If a copy of the MPL was not distributed with this
-* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
-const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
-const {Preferences} = ChromeUtils.import("resource://gre/modules/Preferences.jsm");
-const {ContextualIdentityService} = ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm");
-const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
-const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+const {Helper} = ChromeUtils.importESModule('chrome://juggler/content/Helper.js');
+const {Preferences} = ChromeUtils.importESModule("resource://gre/modules/Preferences.sys.mjs");
+const {ContextualIdentityService} = ChromeUtils.importESModule("resource://gre/modules/ContextualIdentityService.sys.mjs");
+const {NetUtil} = ChromeUtils.importESModule('resource://gre/modules/NetUtil.sys.mjs');
+const {AppConstants} = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs");
const Cr = Components.results;
const helper = new Helper();
-const IDENTITY_NAME = 'Camoufox ';
+const IDENTITY_NAME = 'JUGGLER ';
const HUNDRED_YEARS = 60 * 60 * 24 * 365 * 100;
const ALL_PERMISSIONS = [
@@ -22,6 +21,9 @@ const ALL_PERMISSIONS = [
];
let globalTabAndWindowActivationChain = Promise.resolve();
+// This is a workaround for https://github.com/microsoft/playwright/issues/34586
+let didCreateFirstPage = false;
+let globalNewPageChain = Promise.resolve();
class DownloadInterceptor {
constructor(registry) {
@@ -102,7 +104,7 @@ class DownloadInterceptor {
const screencastService = Cc['@mozilla.org/juggler/screencast;1'].getService(Ci.nsIScreencastService);
-class TargetRegistry {
+export class TargetRegistry {
static instance() {
return TargetRegistry._instance || null;
}
@@ -308,6 +310,16 @@ class TargetRegistry {
}
async newPage({browserContextId}) {
+ // When creating the very first page, we cannot create multiple in parallel.
+ // See https://github.com/microsoft/playwright/issues/34586.
+ if (didCreateFirstPage)
+ return this._newPageInternal({browserContextId});
+ const result = globalNewPageChain.then(() => this._newPageInternal({browserContextId}));
+ globalNewPageChain = result.catch(error => { /* swallow errors to keep chain running */ });
+ return result;
+ }
+
+ async _newPageInternal({browserContextId}) {
const browserContext = this.browserContextForId(browserContextId);
const features = "chrome,dialog=no,all";
// See _callWithURIToLoad in browser.js for the structure of window.arguments
@@ -356,6 +368,7 @@ class TargetRegistry {
if (await target.hasFailedToOverrideTimezone())
throw new Error('Failed to override timezone');
}
+ didCreateFirstPage = true;
return target.id();
}
@@ -372,7 +385,7 @@ class TargetRegistry {
}
}
-class PageTarget {
+export class PageTarget {
constructor(registry, win, tab, browserContext, opener) {
helper.decorateAsEventEmitter(this);
@@ -384,16 +397,7 @@ class PageTarget {
this._linkedBrowser = tab.linkedBrowser;
this._browserContext = browserContext;
this._viewportSize = undefined;
- // Set the viewport size to Camoufox's default value.
- if (
- ChromeUtils.camouGetInt("window.innerWidth")
- || ChromeUtils.camouGetInt("window.innerHeight")
- ) {
- this._viewportSize = {
- width: ChromeUtils.camouGetInt("window.innerWidth") || 1280,
- height: ChromeUtils.camouGetInt("window.innerHeight") || 720,
- };
- }
+ this._zoom = 1;
this._initialDPPX = this._linkedBrowser.browsingContext.overrideDPPX;
this._url = 'about:blank';
this._openerId = opener ? opener.id() : undefined;
@@ -506,9 +510,11 @@ class PageTarget {
this.updateUserAgent(browsingContext);
this.updatePlatform(browsingContext);
this.updateDPPXOverride(browsingContext);
+ this.updateZoom(browsingContext);
this.updateEmulatedMedia(browsingContext);
this.updateColorSchemeOverride(browsingContext);
this.updateReducedMotionOverride(browsingContext);
+ this.updateContrastOverride(browsingContext);
this.updateForcedColorsOverride(browsingContext);
this.updateForceOffline(browsingContext);
this.updateCacheDisabled(browsingContext);
@@ -544,7 +550,16 @@ class PageTarget {
}
updateDPPXOverride(browsingContext = undefined) {
- (browsingContext || this._linkedBrowser.browsingContext).overrideDPPX = this._browserContext.deviceScaleFactor || this._initialDPPX;
+ browsingContext ||= this._linkedBrowser.browsingContext;
+ const dppx = this._zoom * (this._browserContext.deviceScaleFactor || this._initialDPPX);
+ browsingContext.overrideDPPX = dppx;
+ }
+
+ async updateZoom(browsingContext = undefined) {
+ browsingContext ||= this._linkedBrowser.browsingContext;
+ // Update dpr first, and then UI zoom.
+ this.updateDPPXOverride(browsingContext);
+ browsingContext.fullZoom = this._zoom;
}
_updateModalDialogs() {
@@ -579,19 +594,7 @@ class PageTarget {
// The "default size" (1) is only respected when the page is opened.
// Otherwise, explicitly set page viewport prevales over browser context
// default viewport.
-
- // Camoufox is already handling viewport size, so we don't need to set it here.
- if (
- ChromeUtils.camouGetInt("window.outerWidth") ||
- ChromeUtils.camouGetInt("window.outerHeight") ||
- ChromeUtils.camouGetInt("window.innerWidth") ||
- ChromeUtils.camouGetInt("window.innerHeight")
- ) {
- return;
- }
-
const viewportSize = this._viewportSize || this._browserContext.defaultViewportSize;
-
if (viewportSize) {
const {width, height} = viewportSize;
this._linkedBrowser.style.setProperty('width', width + 'px');
@@ -606,7 +609,7 @@ class PageTarget {
const toolbarTop = stackRect.y;
this._window.resizeBy(width - this._window.innerWidth, height + toolbarTop - this._window.innerHeight);
- await this._channel.connect('').send('awaitViewportDimensions', { width, height });
+ await this._channel.connect('').send('awaitViewportDimensions', { width: width / this._zoom, height: height / this._zoom });
} else {
this._linkedBrowser.style.removeProperty('width');
this._linkedBrowser.style.removeProperty('height');
@@ -618,8 +621,8 @@ class PageTarget {
const actualSize = this._linkedBrowser.getBoundingClientRect();
await this._channel.connect('').send('awaitViewportDimensions', {
- width: actualSize.width,
- height: actualSize.height,
+ width: actualSize.width / this._zoom,
+ height: actualSize.height / this._zoom,
});
}
}
@@ -639,7 +642,7 @@ class PageTarget {
}
updateColorSchemeOverride(browsingContext = undefined) {
- (browsingContext || this._linkedBrowser.browsingContext).prefersColorSchemeOverride = this.colorScheme || 'dark';
+ (browsingContext || this._linkedBrowser.browsingContext).prefersColorSchemeOverride = this.colorScheme || this._browserContext.colorScheme || 'none';
}
setReducedMotion(reducedMotion) {
@@ -651,6 +654,15 @@ class PageTarget {
(browsingContext || this._linkedBrowser.browsingContext).prefersReducedMotionOverride = this.reducedMotion || this._browserContext.reducedMotion || 'none';
}
+ setContrast(contrast) {
+ this.contrast = fromProtocolContrast(contrast);
+ this.updateContrastOverride();
+ }
+
+ updateContrastOverride(browsingContext = undefined) {
+ (browsingContext || this._linkedBrowser.browsingContext).prefersContrastOverride = this.contrast || this._browserContext.contrast || 'none';
+ }
+
setForcedColors(forcedColors) {
this.forcedColors = fromProtocolForcedColors(forcedColors);
this.updateForcedColorsOverride();
@@ -672,6 +684,14 @@ class PageTarget {
await this.updateViewportSize();
}
+ async setZoom(zoom) {
+ // This is default range from the ZoomManager.
+ if (zoom < 0.3 || zoom > 5)
+ throw new Error('Invalid zoom value, must be between 0.3 and 5');
+ this._zoom = zoom;
+ await this.updateZoom();
+ }
+
close(runBeforeUnload = false) {
this._gBrowser.removeTab(this._tab, {
skipPermitUnload: !runBeforeUnload,
@@ -878,6 +898,14 @@ function fromProtocolReducedMotion(reducedMotion) {
throw new Error('Unknown reduced motion: ' + reducedMotion);
}
+function fromProtocolContrast(contrast) {
+ if (contrast === 'more' || contrast === 'less' || contrast === 'custom' || contrast === 'no-preference')
+ return contrast;
+ if (contrast === null)
+ return undefined;
+ throw new Error('Unknown contrast: ' + contrast);
+}
+
function fromProtocolForcedColors(forcedColors) {
if (forcedColors === 'active' || forcedColors === 'none')
return forcedColors;
@@ -918,6 +946,7 @@ class BrowserContext {
this.colorScheme = 'none';
this.forcedColors = 'none';
this.reducedMotion = 'none';
+ this.contrast = 'none';
this.videoRecordingOptions = undefined;
this.crossProcessCookie = {
initScripts: [],
@@ -944,6 +973,12 @@ class BrowserContext {
page.updateReducedMotionOverride();
}
+ setContrast(contrast) {
+ this.contrast = fromProtocolContrast(contrast);
+ for (const page of this.pages)
+ page.updateContrastOverride();
+ }
+
setForcedColors(forcedColors) {
this.forcedColors = fromProtocolForcedColors(forcedColors);
for (const page of this.pages)
@@ -988,9 +1023,9 @@ class BrowserContext {
if (ignoreHTTPSErrors) {
Preferences.set("network.stricttransportsecurity.preloadlist", false);
Preferences.set("security.cert_pinning.enforcement_level", 0);
- certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(true, this.userContextId);
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyDataForUserContext(this.userContextId, true);
} else {
- certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(false, this.userContextId);
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyDataForUserContext(this.userContextId, false);
}
}
@@ -1025,15 +1060,6 @@ class BrowserContext {
}
async setDefaultViewport(viewport) {
- // Camoufox: only override the set viewport if a new one was passed
- if (
- ChromeUtils.camouGetInt("window.innerWidth")
- || ChromeUtils.camouGetInt("window.innerHeight")
- ) {
- if (viewport.viewportSize?.width == 1280 && viewport.viewportSize?.height == 720) {
- return;
- }
- }
this.defaultViewportSize = viewport ? viewport.viewportSize : undefined;
this.deviceScaleFactor = viewport ? viewport.deviceScaleFactor : undefined;
await Promise.all(Array.from(this.pages).map(page => page.updateViewportSize()));
@@ -1098,7 +1124,8 @@ class BrowserContext {
setCookies(cookies) {
const protocolToSameSite = {
- [undefined]: Ci.nsICookie.SAMESITE_NONE,
+ [undefined]: Ci.nsICookie.SAMESITE_UNSET,
+ 'None': Ci.nsICookie.SAMESITE_UNSET,
'Lax': Ci.nsICookie.SAMESITE_LAX,
'Strict': Ci.nsICookie.SAMESITE_STRICT,
};
@@ -1126,7 +1153,7 @@ class BrowserContext {
secure,
cookie.httpOnly || false,
cookie.expires === undefined || cookie.expires === -1 /* isSession */,
- cookie.expires === undefined ? Date.now() + HUNDRED_YEARS : cookie.expires,
+ cookie.expires === undefined ? Date.now() + HUNDRED_YEARS : cookie.expires * 1000,
{ userContextId: this.userContextId || undefined } /* originAttributes */,
protocolToSameSite[cookie.sameSite],
Ci.nsICookie.SCHEME_UNSET
@@ -1141,6 +1168,7 @@ class BrowserContext {
getCookies() {
const result = [];
const sameSiteToProtocol = {
+ [Ci.nsICookie.SAMESITE_UNSET]: 'None',
[Ci.nsICookie.SAMESITE_NONE]: 'None',
[Ci.nsICookie.SAMESITE_LAX]: 'Lax',
[Ci.nsICookie.SAMESITE_STRICT]: 'Strict',
@@ -1155,7 +1183,7 @@ class BrowserContext {
value: cookie.value,
domain: cookie.host,
path: cookie.path,
- expires: cookie.isSession ? -1 : cookie.expiry,
+ expires: cookie.isSession ? -1 : cookie.expiry / 1000,
size: cookie.name.length + cookie.value.length,
httpOnly: cookie.isHttpOnly,
secure: cookie.isSecure,
@@ -1267,7 +1295,3 @@ TargetRegistry.Events = {
DownloadFinished: Symbol('TargetRegistry.Events.DownloadFinished'),
ScreencastStopped: Symbol('TargetRegistry.ScreencastStopped'),
};
-
-var EXPORTED_SYMBOLS = ['TargetRegistry', 'PageTarget'];
-this.TargetRegistry = TargetRegistry;
-this.PageTarget = PageTarget;
\ No newline at end of file
diff --git a/additions/juggler/components/Juggler.js b/additions/juggler/components/Juggler.js
index b272815..8ce5f93 100644
--- a/additions/juggler/components/Juggler.js
+++ b/additions/juggler/components/Juggler.js
@@ -2,17 +2,16 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-var EXPORTED_SYMBOLS = ["Juggler", "JugglerFactory"];
+// Services is available as a global in XPCOM component context
-const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
-const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-const {ComponentUtils} = ChromeUtils.import("resource://gre/modules/ComponentUtils.jsm");
-const {Dispatcher} = ChromeUtils.import("chrome://juggler/content/protocol/Dispatcher.js");
-const {BrowserHandler} = ChromeUtils.import("chrome://juggler/content/protocol/BrowserHandler.js");
-const {NetworkObserver} = ChromeUtils.import("chrome://juggler/content/NetworkObserver.js");
-const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
-const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
-const {ActorManagerParent} = ChromeUtils.import('resource://gre/modules/ActorManagerParent.jsm');
+// Load SimpleChannel in browser-process global.
+Services.scriptloader.loadSubScript('chrome://juggler/content/SimpleChannel.js');
+const {Dispatcher} = ChromeUtils.importESModule("chrome://juggler/content/protocol/Dispatcher.js");
+const {BrowserHandler} = ChromeUtils.importESModule("chrome://juggler/content/protocol/BrowserHandler.js");
+const {NetworkObserver} = ChromeUtils.importESModule("chrome://juggler/content/NetworkObserver.js");
+const {TargetRegistry} = ChromeUtils.importESModule("chrome://juggler/content/TargetRegistry.js");
+const {Helper} = ChromeUtils.importESModule('chrome://juggler/content/Helper.js');
+const {ActorManagerParent} = ChromeUtils.importESModule('resource://gre/modules/ActorManagerParent.sys.mjs');
const helper = new Helper();
const Cc = Components.classes;
@@ -22,10 +21,10 @@ const Ci = Components.interfaces;
ActorManagerParent.addJSWindowActors({
JugglerFrame: {
parent: {
- moduleURI: 'chrome://juggler/content/JugglerFrameParent.jsm',
+ esModuleURI: 'chrome://juggler/content/JugglerFrameParent.sys.mjs',
},
child: {
- moduleURI: 'chrome://juggler/content/content/JugglerFrameChild.jsm',
+ esModuleURI: 'chrome://juggler/content/JugglerFrameChild.sys.mjs',
events: {
// Normally, we instantiate an actor when a new window is created.
DOMWindowCreated: {},
@@ -46,7 +45,7 @@ ActorManagerParent.addJSWindowActors({
let browserStartupFinishedCallback;
let browserStartupFinishedPromise = new Promise(x => browserStartupFinishedCallback = x);
-class Juggler {
+export class Juggler {
get classDescription() { return "Sample command-line handler"; }
get classID() { return Components.ID('{f7a74a33-e2ab-422d-b022-4fb213dd2639}'); }
get contractID() { return "@mozilla.org/remote/juggler;1" }
@@ -65,7 +64,7 @@ class Juggler {
}
// This flow is taken from Remote agent and Marionette.
- // See https://github.com/mozilla/gecko-dev/blob/0c1b4921830e6af8bc951da01d7772de2fe60a08/remote/components/RemoteAgent.jsm#L302
+ // See https://github.com/mozilla-firefox/firefox/blob/35e22180b0b61413dd8eccf6c00b1c6fac073eee/remote/components/RemoteAgent.sys.mjs#L417
async observe(subject, topic) {
switch (topic) {
case "profile-after-change":
@@ -135,20 +134,15 @@ class Juggler {
},
};
pipe.init(connection);
- ChromeUtils.camouDebug('Juggler pipe initialized');
const dispatcher = new Dispatcher(connection);
- ChromeUtils.camouDebug('Dispatcher created');
browserHandler = new BrowserHandler(dispatcher.rootSession(), dispatcher, targetRegistry, browserStartupFinishedPromise, () => {
- ChromeUtils.camouDebug('BrowserHandler cleanup callback called');
if (this._silent)
Services.startup.exitLastWindowClosingSurvivalArea();
connection.onclose();
pipe.stop();
pipeStopped = true;
});
- ChromeUtils.camouDebug('BrowserHandler created');
dispatcher.rootSession().setHandler(browserHandler);
- ChromeUtils.camouDebug('BrowserHandler set as root session handler');
loadStyleSheet();
dump(`\nJuggler listening to the pipe\n`);
break;
@@ -160,7 +154,7 @@ class Juggler {
const jugglerInstance = new Juggler();
// This is used by the XPCOM codepath which expects a constructor
-var JugglerFactory = function() {
+export var JugglerFactory = function() {
return jugglerInstance;
};
diff --git a/additions/juggler/components/components.conf b/additions/juggler/components/components.conf
index e5bc652..955b38a 100644
--- a/additions/juggler/components/components.conf
+++ b/additions/juggler/components/components.conf
@@ -11,7 +11,7 @@ Classes = [
"command-line-handler": "m-remote",
"profile-after-change": "Juggler",
},
- "jsm": "chrome://juggler/content/components/Juggler.js",
+ "esModule": "chrome://juggler/content/components/Juggler.js",
"constructor": "JugglerFactory",
},
]
diff --git a/additions/juggler/content/FrameTree.js b/additions/juggler/content/FrameTree.js
index e4a7db1..09a95b3 100644
--- a/additions/juggler/content/FrameTree.js
+++ b/additions/juggler/content/FrameTree.js
@@ -7,13 +7,11 @@ const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
-const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
-const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
-const {Runtime} = ChromeUtils.import('chrome://juggler/content/content/Runtime.js');
+const {Helper} = ChromeUtils.importESModule('chrome://juggler/content/Helper.js');
const helper = new Helper();
-class FrameTree {
+export class FrameTree {
constructor(rootBrowsingContext) {
helper.decorateAsEventEmitter(this);
@@ -411,11 +409,7 @@ class Frame {
this._parentFrame = parentFrame;
parentFrame._children.add(this);
}
-
- this.allowMW = ChromeUtils.camouGetBool('allowMainWorld', false);
- this.forceScopeAccess = ChromeUtils.camouGetBool('forceScopeAccess', false);
- this.masterSandbox = undefined;
this._lastCommittedNavigationId = null;
this._pendingNavigationId = null;
@@ -509,46 +503,18 @@ class Frame {
};
}
- // Camoufox: Add a "God mode" master sandbox with it's own compartment
- getMasterSandbox() {
- if (!this.masterSandbox) {
- this.masterSandbox = Cu.Sandbox(
- Services.scriptSecurityManager.getSystemPrincipal(),
- {
- sandboxPrototype: this.domWindow(),
- wantComponents: false,
- wantExportHelpers: false,
- wantXrays: true,
- freshCompartment: true,
- }
- );
- }
- return this.masterSandbox;
- }
-
- _createIsolatedContext(name, useMaster=false) {
- let sandbox;
- // Camoufox: Use the master sandbox (with system principle scope access)
- if (useMaster && this.forceScopeAccess) {
- sandbox = this.getMasterSandbox();
- } else {
- // Standard access (run in domWindow principal)
- sandbox = Cu.Sandbox([this.domWindow()], {
- sandboxPrototype: this.domWindow(),
- wantComponents: false,
- wantExportHelpers: false,
- wantXrays: true,
- });
- }
+ _createIsolatedContext(name) {
+ const principal = [this.domWindow()]; // extended principal
+ const sandbox = Cu.Sandbox(principal, {
+ sandboxPrototype: this.domWindow(),
+ wantComponents: false,
+ wantExportHelpers: false,
+ wantXrays: true,
+ });
const world = this._runtime.createExecutionContext(this.domWindow(), sandbox, {
frameId: this.id(),
name,
});
- // Camoufox: Create a main world for the isolated context
- if (this.allowMW) {
- const mainWorld = this._runtime.createMW(this.domWindow(), this.domWindow());
- world.mainEquivalent = mainWorld;
- }
this._worldNameToContext.set(name, world);
return world;
}
@@ -582,11 +548,15 @@ class Frame {
webSocketService.removeListener(this._webSocketListenerInnerWindowId, this._webSocketListener);
this._webSocketListenerInnerWindowId = this.domWindow().windowGlobalChild.innerWindowId;
webSocketService.addListener(this._webSocketListenerInnerWindowId, this._webSocketListener);
+
for (const context of this._worldNameToContext.values())
this._runtime.destroyExecutionContext(context);
this._worldNameToContext.clear();
- // Camoufox: Scope the initial execution context to prevent leaks
- this._createIsolatedContext('', true);
+
+ this._worldNameToContext.set('', this._runtime.createExecutionContext(this.domWindow(), this.domWindow(), {
+ frameId: this._frameId,
+ name: '',
+ }));
for (const [name, world] of this._frameTree._isolatedWorlds) {
if (name)
this._createIsolatedContext(name);
@@ -718,6 +688,3 @@ function channelId(channel) {
}
-var EXPORTED_SYMBOLS = ['FrameTree'];
-this.FrameTree = FrameTree;
-
diff --git a/additions/juggler/content/JugglerFrameChild.jsm b/additions/juggler/content/JugglerFrameChild.jsm
index 05d9147..af060c5 100644
--- a/additions/juggler/content/JugglerFrameChild.jsm
+++ b/additions/juggler/content/JugglerFrameChild.jsm
@@ -1,7 +1,7 @@
"use strict";
-const { Helper } = ChromeUtils.import('chrome://juggler/content/Helper.js');
-const { initialize } = ChromeUtils.import('chrome://juggler/content/content/main.js');
+const { Helper } = ChromeUtils.importESModule('chrome://juggler/content/Helper.js');
+const { initialize } = ChromeUtils.importESModule('chrome://juggler/content/content/main.js');
const Ci = Components.interfaces;
const helper = new Helper();
@@ -10,7 +10,7 @@ let sameProcessInstanceNumber = 0;
const topBrowingContextToAgents = new Map();
-class JugglerFrameChild extends JSWindowActorChild {
+export class JugglerFrameChild extends JSWindowActorChild {
constructor() {
super();
@@ -83,4 +83,3 @@ class JugglerFrameChild extends JSWindowActorChild {
receiveMessage() { }
}
-var EXPORTED_SYMBOLS = ['JugglerFrameChild'];
diff --git a/additions/juggler/content/JugglerFrameChild.sys.mjs b/additions/juggler/content/JugglerFrameChild.sys.mjs
new file mode 100644
index 0000000..08766c8
--- /dev/null
+++ b/additions/juggler/content/JugglerFrameChild.sys.mjs
@@ -0,0 +1,84 @@
+"use strict";
+
+const { Helper } = ChromeUtils.importESModule('chrome://juggler/content/Helper.js');
+const { initialize } = ChromeUtils.importESModule('chrome://juggler/content/content/main.js');
+
+const Ci = Components.interfaces;
+const helper = new Helper();
+
+let sameProcessInstanceNumber = 0;
+
+const topBrowingContextToAgents = new Map();
+
+export class JugglerFrameChild extends JSWindowActorChild {
+ constructor() {
+ super();
+
+ this._eventListeners = [];
+ }
+
+ handleEvent(aEvent) {
+ const agents = this._agents();
+ if (!agents)
+ return;
+ if (aEvent.type === 'DOMWillOpenModalDialog') {
+ agents.channel.pause();
+ return;
+ }
+ if (aEvent.type === 'DOMModalDialogClosed') {
+ agents.channel.resumeSoon();
+ return;
+ }
+ if (aEvent.target === this.document) {
+ agents.pageAgent.onWindowEvent(aEvent);
+ agents.frameTree.onWindowEvent(aEvent);
+ }
+ }
+
+ _agents() {
+ return topBrowingContextToAgents.get(this.browsingContext.top);
+ }
+
+ actorCreated() {
+ this.actorName = `content::${this.browsingContext.browserId}/${this.browsingContext.id}/${++sameProcessInstanceNumber}`;
+
+ this._eventListeners.push(helper.addEventListener(this.contentWindow, 'load', event => {
+ this._agents()?.pageAgent.onWindowEvent(event);
+ }));
+
+ if (this.document.documentURI.startsWith('moz-extension://'))
+ return;
+
+ // Child frame events will be forwarded to related top-level agents.
+ if (this.browsingContext.parent)
+ return;
+
+ let agents = topBrowingContextToAgents.get(this.browsingContext);
+ if (!agents) {
+ agents = initialize(this.browsingContext, this.docShell);
+ topBrowingContextToAgents.set(this.browsingContext, agents);
+ }
+ agents.channel.bindToActor(this);
+ agents.actor = this;
+ }
+
+ didDestroy() {
+ helper.removeListeners(this._eventListeners);
+
+ if (this.browsingContext.parent)
+ return;
+
+ const agents = topBrowingContextToAgents.get(this.browsingContext);
+ // The agents are already re-bound to a new actor.
+ if (agents?.actor !== this)
+ return;
+
+ topBrowingContextToAgents.delete(this.browsingContext);
+
+ agents.channel.resetTransport();
+ agents.pageAgent.dispose();
+ agents.frameTree.dispose();
+ }
+
+ receiveMessage() { }
+}
diff --git a/additions/juggler/content/PageAgent.js b/additions/juggler/content/PageAgent.js
index 226bea6..2ff5e6b 100644
--- a/additions/juggler/content/PageAgent.js
+++ b/additions/juggler/content/PageAgent.js
@@ -8,9 +8,9 @@ const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
-const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
-const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
-const {setTimeout} = ChromeUtils.import('resource://gre/modules/Timer.jsm');
+const {Helper} = ChromeUtils.importESModule('chrome://juggler/content/Helper.js');
+const {NetUtil} = ChromeUtils.importESModule('resource://gre/modules/NetUtil.sys.mjs');
+const {setTimeout} = ChromeUtils.importESModule('resource://gre/modules/Timer.sys.mjs');
const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
Ci.nsIDragService
@@ -51,7 +51,7 @@ class WorkerData {
}
}
-class PageAgent {
+export class PageAgent {
constructor(browserChannel, frameTree) {
this._browserChannel = browserChannel;
this._browserPage = browserChannel.connect('page');
@@ -549,7 +549,7 @@ class PageAgent {
false /*aIgnoreRootScrollFrame*/,
0.0 /*pressure*/,
0 /*inputSource*/,
- false /*isDOMEventSynthesized*/,
+ true /*isDOMEventSynthesized*/,
false /*isWidgetEventSynthesized*/,
0 /*buttons*/,
win.windowUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */,
@@ -575,7 +575,7 @@ class PageAgent {
// We crash by using js-ctypes and dereferencing
// a bad pointer. The crash should happen immediately
// upon loading this frame script.
- const { ctypes } = ChromeUtils.import('resource://gre/modules/ctypes.jsm');
+ const { ctypes } = ChromeUtils.importESModule('resource://gre/modules/ctypes.sys.mjs');
ChromeUtils.privateNoteIntentionalCrash();
const zero = new ctypes.intptr_t(8);
const badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
@@ -709,6 +709,3 @@ class PageAgent {
}
}
-var EXPORTED_SYMBOLS = ['PageAgent'];
-this.PageAgent = PageAgent;
-
diff --git a/additions/juggler/content/Runtime.js b/additions/juggler/content/Runtime.js
index a3e2ce2..ad8720a 100644
--- a/additions/juggler/content/Runtime.js
+++ b/additions/juggler/content/Runtime.js
@@ -8,8 +8,8 @@
if (!this.Debugger) {
// Worker has a Debugger defined already.
- const {addDebuggerToGlobal} = ChromeUtils.import("resource://gre/modules/jsdebugger.jsm", {});
- addDebuggerToGlobal(Components.utils.getGlobalForObject(this));
+ const {addDebuggerToGlobal} = ChromeUtils.importESModule("resource://gre/modules/jsdebugger.sys.mjs");
+ addDebuggerToGlobal(Components.utils.getGlobalForObject(globalThis));
}
let lastId = 0;
@@ -99,32 +99,6 @@ class Runtime {
const executionContext = this.findExecutionContext(executionContextId);
if (!executionContext)
throw new Error('Failed to find execution context with id = ' + executionContextId);
-
- // Hijack the utilityScript.evaluate function to evaluate in the main world
- if (
- ChromeUtils.camouGetBool('allowMainWorld', false) &&
- functionDeclaration.includes('utilityScript.evaluate') &&
- args.length >= 4 &&
- args[3].value &&
- typeof args[3].value === 'string' &&
- args[3].value.startsWith('mw:')) {
- ChromeUtils.camouDebug(`Evaluating in main world: ${args[3].value}`);
- const mainWorldScript = args[3].value.substring(3);
-
- // Get the main world execution context
- const mainContext = executionContext.mainEquivalent;
- if (!mainContext) {
- throw new Error(`Main world injection is not enabled.`);
- }
- // Extract arguments for the main world function
- const functionArgs = args[5]?.value?.a || [];
- const exceptionDetails = {};
- const result = mainContext.executeInGlobal(mainWorldScript, functionArgs, exceptionDetails);
- if (!result)
- return {exceptionDetails};
- return {result};
- }
-
const exceptionDetails = {};
let result = await executionContext.evaluateFunction(functionDeclaration, args, exceptionDetails);
if (!result)
@@ -190,7 +164,7 @@ class Runtime {
emitEvent(this.events.onRuntimeError, {
executionContext,
message: message.errorMessage,
- stack: message.stack?.toString() || '',
+ stack: message.stack.toString(),
});
}
},
@@ -308,11 +282,6 @@ class Runtime {
return context;
}
- createMW(domWindow, contextGlobal) {
- const context = new MainWorldContext(this, domWindow, contextGlobal);
- return context;
- }
-
findExecutionContext(executionContextId) {
const executionContext = this._executionContexts.get(executionContextId);
if (!executionContext)
@@ -337,68 +306,6 @@ class Runtime {
}
}
-class MainWorldContext {
- constructor(runtime, domWindow, contextGlobal) {
- this._runtime = runtime;
- this._domWindow = domWindow;
- this._contextGlobal = contextGlobal;
- this._debuggee = runtime._debugger.addDebuggee(contextGlobal);
- }
-
- _getResult(completionValue, exceptionDetails = {}) {
- if (!completionValue) {
- exceptionDetails.text = "Evaluation terminated";
- return {success: false, obj: null};
- }
-
- if (completionValue.throw) {
- const result = this._debuggee.executeInGlobalWithBindings(`
- (function(error) {
- try {
- if (error instanceof Error) {
- return error.toString();
- }
- return String(error);
- } catch(e) {
- return "Unknown error occurred";
- }
- })(e)
- `, { e: completionValue.throw });
-
- exceptionDetails.text = result.return || "Unknown error";
- return {success: false, obj: null};
- }
-
- return {success: true, obj: completionValue.return};
- }
-
- executeInGlobal(script, args = [], exceptionDetails = {}) {
- try {
- const wrappedScript = `
- (() => {
- let _s = (${script});
- let _r = typeof _s === 'function'
- ? _s(${args.map(arg => JSON.stringify(arg)).join(', ')})
- : _s;
- return JSON.stringify({value: _r});
- })()
- `;
-
- const result = this._debuggee.executeInGlobal(wrappedScript);
-
- let {success, obj} = this._getResult(result, exceptionDetails);
- if (!success) {
- return {exceptionDetails};
- }
- return JSON.parse(obj);
- } catch (e) {
- exceptionDetails.text = e.message;
- exceptionDetails.stack = e.stack;
- return {exceptionDetails};
- }
- }
-}
-
class ExecutionContext {
constructor(runtime, domWindow, contextGlobal, auxData) {
this._runtime = runtime;
@@ -431,8 +338,6 @@ class ExecutionContext {
return hasSymbol ? undefined : result;
}).bind(null, JSON.stringify.bind(JSON))`).return;
-
- this.mainEquivalent = undefined;
}
id() {
@@ -691,5 +596,5 @@ function emitEvent(event, ...args) {
listener.call(null, ...args);
}
-var EXPORTED_SYMBOLS = ['Runtime'];
-this.Runtime = Runtime;
+// Export Runtime to global.
+globalThis.Runtime = Runtime;
diff --git a/additions/juggler/content/main.js b/additions/juggler/content/main.js
index 7eaa704..6a3cd02 100644
--- a/additions/juggler/content/main.js
+++ b/additions/juggler/content/main.js
@@ -2,20 +2,28 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
-const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js');
-const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
-const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js');
+// Services is available as a global
+
+// Load SimpleChannel and Runtime in content process's global.
+// NOTE: since these have to exist in both Worker and main threads, and we do
+// not know a way to load ES Modules in worker threads, we have to use the loadSubScript
+// utility instead.
+Services.scriptloader.loadSubScript('chrome://juggler/content/SimpleChannel.js');
+Services.scriptloader.loadSubScript('chrome://juggler/content/content/Runtime.js');
+
+const {Helper} = ChromeUtils.importESModule('chrome://juggler/content/Helper.js');
+const {FrameTree} = ChromeUtils.importESModule('chrome://juggler/content/content/FrameTree.js');
+const {PageAgent} = ChromeUtils.importESModule('chrome://juggler/content/content/PageAgent.js');
const helper = new Helper();
-function initialize(browsingContext, docShell) {
+export function initialize(browsingContext, docShell) {
const data = { channel: undefined, pageAgent: undefined, frameTree: undefined, failedToOverrideTimezone: false };
const applySetting = {
geolocation: (geolocation) => {
if (geolocation) {
- docShell.setGeolocationOverride({
+ browsingContext.setGeolocationServiceOverride({
coords: {
latitude: geolocation.latitude,
longitude: geolocation.longitude,
@@ -25,14 +33,12 @@ function initialize(browsingContext, docShell) {
heading: NaN,
speed: NaN,
},
- address: null,
- timestamp: Date.now()
+ timestamp: Date.now() + 24 * 60 * 60 * 1000, // Make sure it does not expire for a day.
});
} else {
- docShell.setGeolocationOverride(null);
+ browsingContext.setGeolocationServiceOverride();
}
},
-
bypassCSP: (bypassCSP) => {
docShell.bypassCSPEnabled = bypassCSP;
},
@@ -95,16 +101,20 @@ function initialize(browsingContext, docShell) {
},
async awaitViewportDimensions({width, height}) {
- const win = docShell.domWindow;
- if (win.innerWidth === width && win.innerHeight === height)
- return;
await new Promise(resolve => {
- const listener = helper.addEventListener(win, 'resize', () => {
- if (win.innerWidth === width && win.innerHeight === height) {
- helper.removeListeners([listener]);
+ const listeners = [];
+ const check = () => {
+ helper.removeListeners(listeners);
+ if (docShell.domWindow.innerWidth === width && docShell.domWindow.innerHeight === height) {
resolve();
+ return;
}
- });
+ // Note: "domWindow" listeners are often removed upon navigation, as specced.
+ // To survive viewport changes across navigations, re-install listeners upon commit.
+ listeners.push(helper.addEventListener(docShell.domWindow, 'resize', check));
+ listeners.push(helper.addEventListener(data.frameTree, 'navigationcommitted', check));
+ };
+ check();
});
},
@@ -114,6 +124,3 @@ function initialize(browsingContext, docShell) {
return data;
}
-
-var EXPORTED_SYMBOLS = ['initialize'];
-this.initialize = initialize;
diff --git a/additions/juggler/jar.mn b/additions/juggler/jar.mn
index 7f3ecf5..dbb2499 100644
--- a/additions/juggler/jar.mn
+++ b/additions/juggler/jar.mn
@@ -9,15 +9,18 @@ juggler.jar:
content/Helper.js (Helper.js)
content/NetworkObserver.js (NetworkObserver.js)
+ content/ChannelEventSink.sys.mjs (ChannelEventSink.sys.mjs)
content/TargetRegistry.js (TargetRegistry.js)
content/SimpleChannel.js (SimpleChannel.js)
content/JugglerFrameParent.jsm (JugglerFrameParent.jsm)
+ content/JugglerFrameParent.sys.mjs (JugglerFrameParent.sys.mjs)
content/protocol/PrimitiveTypes.js (protocol/PrimitiveTypes.js)
content/protocol/Protocol.js (protocol/Protocol.js)
content/protocol/Dispatcher.js (protocol/Dispatcher.js)
content/protocol/PageHandler.js (protocol/PageHandler.js)
content/protocol/BrowserHandler.js (protocol/BrowserHandler.js)
- content/content/JugglerFrameChild.jsm (content/JugglerFrameChild.jsm)
+ content/JugglerFrameChild.jsm (content/JugglerFrameChild.jsm)
+ content/JugglerFrameChild.sys.mjs (content/JugglerFrameChild.sys.mjs)
content/content/main.js (content/main.js)
content/content/FrameTree.js (content/FrameTree.js)
content/content/PageAgent.js (content/PageAgent.js)
diff --git a/additions/juggler/protocol/BrowserHandler.js b/additions/juggler/protocol/BrowserHandler.js
index 81aeee4..970d8f8 100644
--- a/additions/juggler/protocol/BrowserHandler.js
+++ b/additions/juggler/protocol/BrowserHandler.js
@@ -4,15 +4,16 @@
"use strict";
-const {AddonManager} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
-const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
-const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
-const {PageHandler} = ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js");
-const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+const {AddonManager} = ChromeUtils.importESModule("resource://gre/modules/AddonManager.sys.mjs");
+const {XPIProvider} = ChromeUtils.importESModule("resource://gre/modules/addons/XPIProvider.sys.mjs");
+const {TargetRegistry} = ChromeUtils.importESModule("chrome://juggler/content/TargetRegistry.js");
+const {Helper} = ChromeUtils.importESModule('chrome://juggler/content/Helper.js');
+const {PageHandler} = ChromeUtils.importESModule("chrome://juggler/content/protocol/PageHandler.js");
+const {AppConstants} = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs");
const helper = new Helper();
-class BrowserHandler {
+export class BrowserHandler {
constructor(session, dispatcher, targetRegistry, startCompletePromise, onclose) {
this._session = session;
this._dispatcher = dispatcher;
@@ -147,6 +148,10 @@ class BrowserHandler {
]);
}
await this._startCompletePromise;
+ await Promise.all([
+ ...XPIProvider.startupPromises,
+ ...XPIProvider.enabledAddonsStartupPromises,
+ ]);
this._onclose();
Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
}
@@ -166,7 +171,9 @@ class BrowserHandler {
['Browser.clearCache']() {
// Clearing only the context cache does not work: https://bugzilla.mozilla.org/show_bug.cgi?id=1819147
Services.cache2.clear();
- ChromeUtils.clearStyleSheetCache();
+ ChromeUtils.clearResourceCache({
+ types: ["stylesheet"],
+ });
}
['Browser.setHTTPCredentials']({browserContextId, credentials}) {
@@ -220,7 +227,7 @@ class BrowserHandler {
}
async ['Browser.setContrast']({browserContextId, contrast}) {
- return; // TODO: Implement
+ await this._targetRegistry.browserContextForId(browserContextId).setContrast(nullToUndefined(contrast));
}
async ['Browser.setVideoRecordingOptions']({browserContextId, options}) {
@@ -313,6 +320,3 @@ async function waitForWindowClosed(browserWindow) {
function nullToUndefined(value) {
return value === null ? undefined : value;
}
-
-var EXPORTED_SYMBOLS = ['BrowserHandler'];
-this.BrowserHandler = BrowserHandler;
diff --git a/additions/juggler/protocol/Dispatcher.js b/additions/juggler/protocol/Dispatcher.js
index cf24b89..7a65d0a 100644
--- a/additions/juggler/protocol/Dispatcher.js
+++ b/additions/juggler/protocol/Dispatcher.js
@@ -2,14 +2,13 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-const {protocol, checkScheme} = ChromeUtils.import("chrome://juggler/content/protocol/Protocol.js");
-const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
+const {protocol} = ChromeUtils.importESModule("chrome://juggler/content/protocol/Protocol.js");
+const {checkScheme} = ChromeUtils.importESModule("chrome://juggler/content/protocol/PrimitiveTypes.js");
+const {Helper} = ChromeUtils.importESModule('chrome://juggler/content/Helper.js');
const helper = new Helper();
-// Camoufox: Exclude redundant internal events from logs.
-const EXCLUDED_DBG = ['Page.navigationStarted', 'Page.frameAttached', 'Runtime.executionContextCreated', 'Runtime.console', 'Page.navigationAborted', 'Page.eventFired'];
-class Dispatcher {
+export class Dispatcher {
/**
* @param {Connection} connection
*/
@@ -46,11 +45,6 @@ class Dispatcher {
async _dispatch(event) {
const data = JSON.parse(event.data);
-
- if (ChromeUtils.isCamouDebug())
- ChromeUtils.camouDebug(`[${new Date().toLocaleString()}]`
- + `\nReceived message: ${safeJsonStringify(data)}`);
-
const id = data.id;
const sessionId = data.sessionId;
delete data.sessionId;
@@ -93,13 +87,6 @@ class Dispatcher {
_emitEvent(sessionId, eventName, params) {
const [domain, eName] = eventName.split('.');
-
- // Camoufox: Log internal events
- if (ChromeUtils.isCamouDebug() && !EXCLUDED_DBG.includes(eventName) && domain !== 'Network') {
- ChromeUtils.camouDebug(`[${new Date().toLocaleString()}]`
- + `\nInternal event: ${eventName}\nParams: ${JSON.stringify(params, null, 2)}`);
- }
-
const scheme = protocol.domains[domain] ? protocol.domains[domain].events[eName] : null;
if (!scheme)
throw new Error(`ERROR: event '${eventName}' is not supported`);
@@ -146,48 +133,3 @@ class ProtocolSession {
return await this._handler[method](params);
}
}
-
-this.EXPORTED_SYMBOLS = ['Dispatcher'];
-this.Dispatcher = Dispatcher;
-
-
-function formatDate(date) {
- const pad = (num) => String(num).padStart(2, '0');
- return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
-}
-
-function truncateObject(obj, maxDepth = 8, maxLength = 100) {
- if (maxDepth < 0) return '[Max Depth Reached]';
-
- if (typeof obj !== 'object' || obj === null) {
- return typeof obj === 'string' ? truncateString(obj, maxLength) : obj;
- }
-
- if (Array.isArray(obj)) {
- return obj.slice(0, 10).map(item => truncateObject(item, maxDepth - 1, maxLength));
- }
-
- const truncated = {};
- for (const [key, value] of Object.entries(obj)) {
- if (Object.keys(truncated).length >= 10) {
- truncated['...'] = '[Truncated]';
- break;
- }
- truncated[key] = truncateObject(value, maxDepth - 1, maxLength);
- }
- return truncated;
-}
-
-function truncateString(str, maxLength) {
- if (str.length <= maxLength) return str;
- ChromeUtils.camouDebug(`String length: ${str.length}`);
- return str.substr(0, maxLength) + '... [truncated]';
-}
-
-function safeJsonStringify(data) {
- try {
- return JSON.stringify(truncateObject(data), null, 2);
- } catch (error) {
- return `[Unable to stringify: ${error.message}]`;
- }
-}
\ No newline at end of file
diff --git a/additions/juggler/protocol/PageHandler.js b/additions/juggler/protocol/PageHandler.js
index 213ede0..1dab7d8 100644
--- a/additions/juggler/protocol/PageHandler.js
+++ b/additions/juggler/protocol/PageHandler.js
@@ -1,14 +1,14 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
-* License, v. 2.0. If a copy of the MPL was not distributed with this
-* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-const {Helper, EventWatcher} = ChromeUtils.import('chrome://juggler/content/Helper.js');
-const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
-const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js');
-const {PageTarget} = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js');
-const {setTimeout} = ChromeUtils.import('resource://gre/modules/Timer.jsm');
+const {Helper, EventWatcher} = ChromeUtils.importESModule('chrome://juggler/content/Helper.js');
+const {NetUtil} = ChromeUtils.importESModule('resource://gre/modules/NetUtil.sys.mjs');
+const {NetworkObserver, PageNetwork} = ChromeUtils.importESModule('chrome://juggler/content/NetworkObserver.js');
+const {PageTarget} = ChromeUtils.importESModule('chrome://juggler/content/TargetRegistry.js');
+const {setTimeout} = ChromeUtils.importESModule('resource://gre/modules/Timer.sys.mjs');
const Cc = Components.classes;
const Ci = Components.interfaces;
@@ -65,7 +65,7 @@ class WorkerHandler {
}
}
-class PageHandler {
+export class PageHandler {
constructor(target, session, contentChannel) {
this._session = session;
this._contentChannel = contentChannel;
@@ -80,17 +80,7 @@ class PageHandler {
}
this._isDragging = false;
-
- // Camoufox: set a random default cursor position
- let random_val = (max_val) => Math.floor(Math.random() * max_val);
-
- // Try to fetch the viewport size
- this._defaultCursorPos = {
- x: random_val(this._pageTarget._viewportSize?.width || 1280),
- y: random_val(this._pageTarget._viewportSize?.height || 720),
- };
- this._lastMousePosition = { ...this._defaultCursorPos };
- this._lastTrackedPos = { ...this._defaultCursorPos };
+ this._lastMousePosition = { x: 0, y: 0 };
this._reportedFrameIds = new Set();
this._networkEventsForUnreportedFrameIds = new Map();
@@ -250,6 +240,10 @@ class PageHandler {
await this._pageTarget.setViewportSize(viewportSize === null ? undefined : viewportSize);
}
+ async ['Page.setZoom']({zoom}) {
+ await this._pageTarget.setZoom(zoom);
+ }
+
async ['Runtime.evaluate'](options) {
return await this._contentPage.send('evaluate', options);
}
@@ -308,10 +302,11 @@ class PageHandler {
return await this._contentPage.send('setFileInputFiles', options);
}
- async ['Page.setEmulatedMedia']({colorScheme, type, reducedMotion, forcedColors}) {
+ async ['Page.setEmulatedMedia']({colorScheme, type, reducedMotion, forcedColors, contrast}) {
this._pageTarget.setColorScheme(colorScheme || null);
this._pageTarget.setReducedMotion(reducedMotion || null);
this._pageTarget.setForcedColors(forcedColors || null);
+ this._pageTarget.setContrast(contrast || null);
this._pageTarget.setEmulatedMedia(type);
}
@@ -432,14 +427,6 @@ class PageHandler {
});
unsubscribe();
- if (ChromeUtils.camouGetBool('memorysaver', false)) {
- ChromeUtils.camouDebug('Clearing all memory...');
- Services.obs.notifyObservers(null, "child-gc-request");
- Cu.forceGC();
- Services.obs.notifyObservers(null, "child-cc-request");
- Cu.forceCC();
- }
-
return {
navigationId: sameDocumentNavigation ? null : navigationId,
};
@@ -518,43 +505,28 @@ class PageHandler {
await helper.awaitTopic('apz-repaints-flushed');
const watcher = new EventWatcher(this._pageEventSink, types, this._pendingEventWatchers);
- const sendMouseEvent = async (eventType, eventX, eventY) => {
+ const promises = [];
+ for (const type of types) {
+ // This dispatches to the renderer synchronously.
const jugglerEventId = win.windowUtils.jugglerSendMouseEvent(
- eventType,
- eventX + boundingBox.left,
- eventY + boundingBox.top,
+ type,
+ x + boundingBox.left,
+ y + boundingBox.top,
button,
clickCount,
modifiers,
false /* aIgnoreRootScrollFrame */,
0.0 /* pressure */,
0 /* inputSource */,
- false /* isDOMEventSynthesized */,
+ true /* isDOMEventSynthesized */,
false /* isWidgetEventSynthesized */,
buttons,
win.windowUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */,
false /* disablePointerEvent */
);
- await watcher.ensureEvent(eventType, eventObject => eventObject.jugglerEventId === jugglerEventId);
- };
- for (const type of types) {
- if (type === 'mousemove' && ChromeUtils.camouGetBool('humanize', false)) {
- let trajectory = ChromeUtils.camouGetMouseTrajectory(this._lastTrackedPos.x, this._lastTrackedPos.y, x, y);
- for (let i = 2; i < trajectory.length - 2; i += 2) {
- let currentX = trajectory[i];
- let currentY = trajectory[i + 1];
- // Skip movement that is out of bounds
- if (currentX < 0 || currentY < 0 || currentX > boundingBox.width || currentY > boundingBox.height) {
- continue;
- }
- await sendMouseEvent(type, currentX, currentY);
- await new Promise(resolve => setTimeout(resolve, 10));
- }
- } else {
- // Call the function for the current event
- await sendMouseEvent(type, x, y);
- }
+ promises.push(watcher.ensureEvent(type, eventObject => eventObject.jugglerEventId === jugglerEventId));
}
+ await Promise.all(promises);
await watcher.dispose();
};
@@ -575,15 +547,15 @@ class PageHandler {
// NOTE: since this won't go inside the renderer, there's no need to wait for ACK.
win.windowUtils.sendMouseEvent(
'mousemove',
- this._defaultCursorPos.x,
- this._defaultCursorPos.y,
+ 0 /* x */,
+ 0 /* y */,
button,
clickCount,
modifiers,
false /* aIgnoreRootScrollFrame */,
0.0 /* pressure */,
0 /* inputSource */,
- false /* isDOMEventSynthesized */,
+ true /* isDOMEventSynthesized */,
false /* isWidgetEventSynthesized */,
buttons,
win.windowUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */,
@@ -612,7 +584,6 @@ class PageHandler {
const watcher = new EventWatcher(this._pageEventSink, ['dragstart', 'juggler-drag-finalized'], this._pendingEventWatchers);
await sendEvents(['mousemove']);
- this._lastTrackedPos = { x, y };
// The order of events after 'mousemove' is sent:
// 1. [dragstart] - might or might NOT be emitted
@@ -720,6 +691,3 @@ class PageHandler {
return await worker.sendMessage(JSON.parse(message));
}
}
-
-var EXPORTED_SYMBOLS = ['PageHandler'];
-this.PageHandler = PageHandler;
diff --git a/additions/juggler/protocol/PrimitiveTypes.js b/additions/juggler/protocol/PrimitiveTypes.js
index 5799038..ca6d1ec 100644
--- a/additions/juggler/protocol/PrimitiveTypes.js
+++ b/additions/juggler/protocol/PrimitiveTypes.js
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-const t = {};
+export const t = {};
t.String = function(x, details = {}, path = ['']) {
if (typeof x === 'string' || typeof x === 'String')
@@ -96,7 +96,7 @@ function beauty(path, obj) {
return `property "${path.join('.')}" - ${JSON.stringify(obj, null, 2)}`;
}
-function checkScheme(scheme, x, details = {}, path = ['']) {
+export function checkScheme(scheme, x, details = {}, path = ['']) {
if (!scheme)
throw new Error(`ILLDEFINED SCHEME: ${path.join('.')}`);
if (typeof scheme === 'object') {
@@ -142,6 +142,3 @@ test(t.Either(t.String, t.Number), {});
*/
-this.t = t;
-this.checkScheme = checkScheme;
-this.EXPORTED_SYMBOLS = ['t', 'checkScheme'];
diff --git a/additions/juggler/protocol/Protocol.js b/additions/juggler/protocol/Protocol.js
index 08ca173..4cb44d6 100644
--- a/additions/juggler/protocol/Protocol.js
+++ b/additions/juggler/protocol/Protocol.js
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js');
+const {t} = ChromeUtils.importESModule('chrome://juggler/content/protocol/PrimitiveTypes.js');
// Protocol-specific types.
const browserTypes = {};
@@ -800,6 +800,11 @@ const Page = {
viewportSize: t.Nullable(pageTypes.Size),
},
},
+ 'setZoom': {
+ params: {
+ zoom: t.Number,
+ },
+ },
'bringToFront': {
params: {
},
@@ -810,6 +815,7 @@ const Page = {
colorScheme: t.Optional(t.Enum(['dark', 'light', 'no-preference'])),
reducedMotion: t.Optional(t.Enum(['reduce', 'no-preference'])),
forcedColors: t.Optional(t.Enum(['active', 'none'])),
+ contrast: t.Optional(t.Enum(['less', 'more', 'custom', 'no-preference'])),
},
},
'setCacheDisabled': {
@@ -1012,8 +1018,6 @@ const Accessibility = {
}
}
-this.protocol = {
+export const protocol = {
domains: {Browser, Heap, Page, Runtime, Network, Accessibility},
};
-this.checkScheme = checkScheme;
-this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme'];
diff --git a/additions/juggler/screencast/nsScreencastService.cpp b/additions/juggler/screencast/nsScreencastService.cpp
index 062a851..fa9d1a9 100644
--- a/additions/juggler/screencast/nsScreencastService.cpp
+++ b/additions/juggler/screencast/nsScreencastService.cpp
@@ -4,6 +4,7 @@
#include "nsScreencastService.h"
+#include "gfxPlatform.h"
#include "ScreencastEncoder.h"
#include "HeadlessWidget.h"
#include "HeadlessWindowCapturer.h"
@@ -343,10 +344,17 @@ nsresult nsScreencastService::StartVideoRecording(nsIScreencastServiceClient* aC
return NS_ERROR_FAILURE;
gfx::IntMargin margin;
- auto bounds = widget->GetScreenBounds().ToUnknownRect();
+ // Screen bounds is the widget location on screen.
+ auto screenBounds = widget->GetScreenBounds().ToUnknownRect();
+ // Client bounds is the content location, in terms of parent widget.
+ // To use it, we need to translate it to screen coordinates first.
auto clientBounds = widget->GetClientBounds().ToUnknownRect();
+ for (auto parent = widget->GetParent(); parent != nullptr; parent = parent->GetParent()) {
+ auto pb = parent->GetClientBounds().ToUnknownRect();
+ clientBounds.MoveBy(pb.X(), pb.Y());
+ }
// Crop the image to exclude frame (if any).
- margin = bounds - clientBounds;
+ margin = screenBounds - clientBounds;
// Crop the image to exclude controls.
margin.top += offsetTop;
diff --git a/bundle/fonts/macos/ヒラギノ丸ゴ ProN W4.ttc b/bundle/fonts/macos/ヒラギノ丸ゴ ProN W4.ttc
deleted file mode 100644
index 5c582fe..0000000
Binary files a/bundle/fonts/macos/ヒラギノ丸ゴ ProN W4.ttc and /dev/null differ
diff --git a/bundle/fonts/macos/ヒラギノ明朝 ProN.ttc b/bundle/fonts/macos/ヒラギノ明朝 ProN.ttc
deleted file mode 100644
index 9c7e273..0000000
Binary files a/bundle/fonts/macos/ヒラギノ明朝 ProN.ttc and /dev/null differ
diff --git a/bundle/fonts/macos/ヒラギノ角ゴシック W0.ttc b/bundle/fonts/macos/ヒラギノ角ゴシック W0.ttc
deleted file mode 100644
index 2be662e..0000000
Binary files a/bundle/fonts/macos/ヒラギノ角ゴシック W0.ttc and /dev/null differ
diff --git a/bundle/fonts/macos/ヒラギノ角ゴシック W1.ttc b/bundle/fonts/macos/ヒラギノ角ゴシック W1.ttc
deleted file mode 100644
index 689735d..0000000
Binary files a/bundle/fonts/macos/ヒラギノ角ゴシック W1.ttc and /dev/null differ
diff --git a/bundle/fonts/macos/ヒラギノ角ゴシック W2.ttc b/bundle/fonts/macos/ヒラギノ角ゴシック W2.ttc
deleted file mode 100644
index 31bbbcb..0000000
Binary files a/bundle/fonts/macos/ヒラギノ角ゴシック W2.ttc and /dev/null differ
diff --git a/bundle/fonts/macos/ヒラギノ角ゴシック W3.ttc b/bundle/fonts/macos/ヒラギノ角ゴシック W3.ttc
deleted file mode 100644
index e4ca975..0000000
Binary files a/bundle/fonts/macos/ヒラギノ角ゴシック W3.ttc and /dev/null differ
diff --git a/bundle/fonts/macos/ヒラギノ角ゴシック W4.ttc b/bundle/fonts/macos/ヒラギノ角ゴシック W4.ttc
deleted file mode 100644
index f4cb99a..0000000
Binary files a/bundle/fonts/macos/ヒラギノ角ゴシック W4.ttc and /dev/null differ
diff --git a/bundle/fonts/macos/ヒラギノ角ゴシック W5.ttc b/bundle/fonts/macos/ヒラギノ角ゴシック W5.ttc
deleted file mode 100644
index 2224690..0000000
Binary files a/bundle/fonts/macos/ヒラギノ角ゴシック W5.ttc and /dev/null differ
diff --git a/bundle/fonts/macos/ヒラギノ角ゴシック W6.ttc b/bundle/fonts/macos/ヒラギノ角ゴシック W6.ttc
deleted file mode 100644
index ab61e3d..0000000
Binary files a/bundle/fonts/macos/ヒラギノ角ゴシック W6.ttc and /dev/null differ
diff --git a/bundle/fonts/macos/ヒラギノ角ゴシック W7.ttc b/bundle/fonts/macos/ヒラギノ角ゴシック W7.ttc
deleted file mode 100644
index 046bf77..0000000
Binary files a/bundle/fonts/macos/ヒラギノ角ゴシック W7.ttc and /dev/null differ
diff --git a/bundle/fonts/macos/ヒラギノ角ゴシック W8.ttc b/bundle/fonts/macos/ヒラギノ角ゴシック W8.ttc
deleted file mode 100644
index 2e70ef5..0000000
Binary files a/bundle/fonts/macos/ヒラギノ角ゴシック W8.ttc and /dev/null differ
diff --git a/bundle/fonts/macos/ヒラギノ角ゴシック W9.ttc b/bundle/fonts/macos/ヒラギノ角ゴシック W9.ttc
deleted file mode 100644
index 78ec8fd..0000000
Binary files a/bundle/fonts/macos/ヒラギノ角ゴシック W9.ttc and /dev/null differ
diff --git a/docs/patch-upgrading-guide.md b/docs/patch-upgrading-guide.md
new file mode 100644
index 0000000..457b145
--- /dev/null
+++ b/docs/patch-upgrading-guide.md
@@ -0,0 +1,596 @@
+# Firefox Patch Upgrading Guide for LLMs
+
+This guide provides step-by-step instructions for updating Camoufox patches when upgrading Firefox versions. Patches frequently break due to Firefox API changes, file reorganizations, and line number shifts.
+
+**All patches are located in the `patches/` directory.** There are no separate context patches to merge—per-context functionality is already built into the patches.
+
+## Table of Contents
+
+1. [Understanding the Patch System](#understanding-the-patch-system)
+2. [Preparation](#preparation)
+3. [General Workflow](#general-workflow)
+4. [Fixing Common Reject Types](#fixing-common-reject-types)
+5. [Context Patch Merging](#context-patch-merging) *(Historical - skip for future updates)*
+6. [Testing and Validation](#testing-and-validation)
+7. [Best Practices](#best-practices)
+
+---
+
+## Understanding the Patch System
+
+### Patch Categories
+
+All Camoufox patches are in the `patches/` directory:
+
+- **Core Patches**: `0-playwright.patch`, `1-leak-fixes.patch`, etc.
+- **Feature Patches**: `webrtc-ip-spoofing.patch`, `anti-font-fingerprinting.patch`, etc.
+- All patches now include per-user-context (per-Playwright-context) support built-in
+
+**Historical Note**: Context patches (e.g., `font-fingerprinting.context.patch`, `webrtc.context.patch`) were previously separate but have been merged into their base patches as of Firefox 146. You will not find `.context.patch` files in the repository.
+
+### Key Infrastructure Files
+
+- **RoverfoxStorageManager.cpp/h**: Thread-safe key-value storage for per-context data
+- **Manager Classes**: FontSpacingSeedManager, WebRTCIPManager, etc.
+- **Window.webidl**: Exposes JavaScript APIs to Playwright
+
+---
+
+## Preparation
+
+### 1. Reset to Clean State
+
+**IMPORTANT**: Always use `make clean` to reset to fresh Firefox source:
+
+```bash
+make clean
+```
+
+**DO NOT** use `git reset` or `git clean` commands directly in the Firefox source directory - these can delete untracked files needed for the build.
+
+### 2. Identify Patches to Update
+
+Check which patches exist:
+
+```bash
+ls patches/*.patch
+```
+
+### 3. Understand Patch Dependencies
+
+Some patches depend on others being applied first:
+- `1-leak-fixes.patch` requires `0-playwright.patch`
+- Check the Makefile or patch comments for dependency chains
+
+---
+
+## General Workflow
+
+### Step 1: Apply Base Patch and Identify Rejects
+
+```bash
+cd camoufox-
+patch -p1 < ../patches/patch-name.patch
+```
+
+Find reject files:
+
+```bash
+find . -name '*.rej' -type f
+```
+
+### Step 2: Analyze Each Reject File
+
+Read the reject file to understand what failed:
+
+```bash
+cat path/to/file.cpp.rej
+```
+
+Reject files show:
+- `@@` lines: Line numbers where patch expected to apply
+- `-` lines: What the patch expected to find (old code)
+- `+` lines: What the patch wanted to add (new code)
+
+### Step 3: Locate the Correct Position in Firefox Code
+
+The line numbers in rejects are usually wrong for the new Firefox version. You need to:
+
+1. **Search for unique context** around the reject
+2. **Understand what the patch is doing**
+3. **Find equivalent location** in new Firefox code
+
+### Step 4: Apply Changes Manually
+
+Use the Edit tool to apply the rejected changes to the correct location.
+
+### Step 5: Remove Reject Files
+
+After fixing all rejects:
+
+```bash
+rm -f path/to/file.cpp.rej
+find . -name '*.rej' -type f # Verify all removed
+```
+
+### Step 6: Generate Updated Patch
+
+```bash
+# Add any new files first
+git add new/file.cpp new/file.h
+
+# Generate patch with both staged and unstaged changes
+git diff --cached --binary > /tmp/patch-name.patch
+git diff --binary >> /tmp/patch-name.patch
+
+# Copy to patches directory
+cp /tmp/patch-name.patch ../patches/patch-name.patch
+```
+
+### Step 7: Verify Patch Applies Cleanly
+
+```bash
+cd ..
+make clean
+cd camoufox-
+patch -p1 < ../patches/patch-name.patch
+find . -name '*.rej' -type f # Should return nothing
+```
+
+---
+
+## Fixing Common Reject Types
+
+### Type 1: Include Directive Rejects
+
+**Symptom**: Reject shows failed `#include` additions
+
+**Example Reject**:
+```
+@@ -325,6 +325,7 @@
+ #include "xpcpublic.h"
+
++#include "WebRTCIPManager.h"
+ #include "nsDocShell.h"
+```
+
+**How to Fix**:
+
+1. Read the actual file to find the includes section
+2. Search for nearby includes (e.g., `xpcpublic.h`)
+3. Add the new include in the appropriate location
+4. Firefox include order: system headers, then Mozilla headers, alphabetically within groups
+
+**Example**:
+
+```cpp
+// Find this in the actual file:
+#include "xpcpublic.h"
+
+// Add the missing includes after it:
+#include "xpcpublic.h"
+
+#include "WebRTCIPManager.h"
+#include "nsDocShell.h"
+#include "mozilla/OriginAttributes.h"
+```
+
+### Type 2: Function Signature Changes
+
+**Symptom**: Reject shows function call with changed parameters
+
+**Example Reject**:
+```
+- mouseOrPointerEvent.mButton = aButton;
++ mouseOrPointerEvent.mJugglerEventId = aMouseEventData.mJugglerEventId;
+```
+
+**Common Causes**:
+- Firefox refactored the API
+- Parameters moved from individual args to struct/data object
+- Parameter order changed
+
+**How to Fix**:
+
+1. Search for the function definition in Firefox source
+2. Understand the new API structure
+3. Port the patch logic to the new API
+
+**Example - Firefox 146 Mouse Event Refactoring**:
+
+Old Firefox 144 API (individual parameters):
+```cpp
+void SynthesizeMouseEvent(int x, int y, int button, ...)
+```
+
+New Firefox 146 API (structured data):
+```cpp
+void SynthesizeMouseEvent(SynthesizeMouseEventData& aData,
+ SynthesizeMouseEventOptions& aOptions)
+```
+
+Port the patch:
+```cpp
+// Old patch code:
+mouseEvent.mButton = aButton;
+mouseEvent.jugglerEventId = aJugglerEventId;
+
+// New patch code for Firefox 146:
+mouseOrPointerEvent.mButton = aMouseEventData.mButton;
+mouseOrPointerEvent.mJugglerEventId = aMouseEventData.mJugglerEventId;
+mouseOrPointerEvent.convertToPointer = aOptions.mConvertToPointer;
+```
+
+### Type 3: Missing Context - Code Moved
+
+**Symptom**: Reject shows context that doesn't exist in the file
+
+**How to Fix**:
+
+1. Use grep to search for unique function names or variables in the reject
+2. Find where Firefox moved the code
+3. Apply the patch to the new location
+
+```bash
+# Search across the codebase
+grep -r "FunctionName" camoufox-/ --include="*.cpp"
+```
+
+### Type 4: New Parameter Added to Function Calls
+
+**Symptom**: Reject shows function call, but Firefox added/removed parameters
+
+**Example - MakeTextRun userContextId**:
+
+Old call:
+```cpp
+MakeTextRun(text, len, drawTarget, appUnitsPerDevPixel, flags, recorder);
+```
+
+New Firefox expects:
+```cpp
+MakeTextRun(text, len, drawTarget, appUnitsPerDevPixel, flags, recorder, userContextId);
+```
+
+**How to Fix**:
+
+1. Extract userContextId from available context (Document, PresContext, etc.)
+2. Add proper extraction code before the call
+3. Pass userContextId as the last parameter
+
+**Standard userContextId Extraction Pattern**:
+
+```cpp
+uint32_t userContextId = 0;
+if (mozilla::dom::Document* doc = presContext->Document()) {
+ if (nsIPrincipal* principal = doc->NodePrincipal()) {
+ auto* bp = mozilla::BasePrincipal::Cast(principal);
+ if (bp) {
+ userContextId = bp->OriginAttributesRef().mUserContextId;
+ }
+ }
+}
+
+// Now pass userContextId to the function
+MakeTextRun(..., userContextId);
+```
+
+### Type 5: Line Number Shifts (No Code Changes)
+
+**Symptom**: Reject shows patch tried to apply at wrong line number, but code is identical
+
+**How to Fix**:
+
+Simply apply the patch manually at the correct line number. The code hasn't changed, just the location.
+
+---
+
+## Context Patch Merging (Historical - Not Applicable for Future Updates)
+
+**NOTE**: As of Firefox 146, all context patches have been merged into their base patches. This section is kept for historical reference and understanding how the patches evolved. Future Firefox updates will only need to update patches in the `patches/` directory.
+
+---
+
+**Historical Context**: Context patches previously added per-user-context functionality to base patches. The workflow was different from simple patch updates.
+
+### Historical Goal
+
+Merge all changes from `*.context.patch` into the corresponding base patch so there's only one comprehensive patch file.
+
+### Historical Example: font-fingerprinting.context.patch → anti-font-fingerprinting.patch
+
+### Workflow
+
+1. **Reset to clean Firefox**:
+ ```bash
+ make clean
+ ```
+
+2. **Apply base patch first**:
+ ```bash
+ cd camoufox-
+ patch -p1 < ../patches/anti-font-fingerprinting.patch
+ ```
+
+3. **Apply context patch on top**:
+ ```bash
+ patch -p1 < ../font-fingerprinting.context.patch
+ ```
+
+4. **Fix any rejects** (usually include conflicts since base patch may have some overlapping changes)
+
+5. **Generate combined patch**:
+ ```bash
+ # Add new files (e.g., FontSpacingSeedManager.cpp/h)
+ git add dom/base/FontSpacingSeedManager.cpp
+ git add dom/base/FontSpacingSeedManager.h
+ git add dom/base/RoverfoxStorageManager.cpp
+ git add dom/base/RoverfoxStorageManager.h
+
+ # Generate combined patch
+ git diff --cached --binary > /tmp/anti-font-fingerprinting.patch
+ git diff --binary >> /tmp/anti-font-fingerprinting.patch
+
+ # Replace base patch
+ cp /tmp/anti-font-fingerprinting.patch ../patches/anti-font-fingerprinting.patch
+ ```
+
+6. **Verify combined patch**:
+ ```bash
+ cd ..
+ make clean
+ cd camoufox-
+ patch -p1 < ../patches/anti-font-fingerprinting.patch
+ find . -name '*.rej' -type f # Should be empty
+ ```
+
+### What Context Patches Add
+
+Context patches typically add:
+
+1. **Manager classes** (e.g., FontSpacingSeedManager, WebRTCIPManager):
+ - Store per-context settings using RoverfoxStorageManager
+ - Provide WebIDL-compatible enable/disable checks
+ - Handle self-destructing functions
+
+2. **Window.webidl functions**:
+ - JavaScript APIs exposed to Playwright
+ - Examples: `setFontSpacingSeed()`, `setWebRTCIPv4()`
+
+3. **nsGlobalWindowInner.cpp implementations**:
+ - Extract userContextId from window/document/docshell
+ - Call manager classes
+ - Self-destruct logic (remove function after first use)
+
+4. **Core logic changes**:
+ - Replace global config (MaskConfig) with per-context manager
+ - Pass userContextId through call chains
+ - Query manager for per-context values
+
+---
+
+## Testing and Validation
+
+### Minimal Verification
+
+After updating a patch, always verify:
+
+1. **Patch applies cleanly**:
+ ```bash
+ make clean
+ cd camoufox-
+ patch -p1 < ../patches/patch-name.patch
+ find . -name '*.rej' -type f
+ ```
+
+2. **No reject files remain**
+
+3. **Build compiles** (if feasible):
+ ```bash
+ cd ..
+ make build
+ ```
+
+### Full Testing
+
+For critical patches, test with actual Playwright scenarios after building.
+
+---
+
+## Best Practices
+
+### DO:
+
+1. ✅ **Always use `make clean`** to reset Firefox source
+2. ✅ **Read and understand** what the patch is trying to do before fixing rejects
+3. ✅ **Search for API changes** in Firefox release notes when functions have changed
+4. ✅ **Use grep/search** extensively to find where code moved
+5. ✅ **Extract userContextId properly** using the standard pattern
+6. ✅ **Test patches apply cleanly** before considering them done
+7. ✅ **Keep commits atomic** - one patch fix per session
+8. ✅ **Document major API changes** you discover
+
+### DON'T:
+
+1. ❌ **Don't use `git reset` or `git clean`** on Firefox source directory
+2. ❌ **Don't leave TODO comments** - fix things properly as you go
+3. ❌ **Don't guess parameter values** - extract them properly or investigate
+4. ❌ **Don't skip verification** - always test the patch applies cleanly
+5. ❌ **Don't batch multiple patch updates** - do them one at a time
+6. ❌ **Don't assume line numbers are correct** in reject files
+7. ❌ **Don't ignore warnings** during patch application
+
+### Common Pitfalls
+
+1. **Assuming reject line numbers are accurate**: They're usually wrong in new Firefox versions
+2. **Not understanding API changes**: Firefox refactors often - read the new code
+3. **Forgetting to add new files**: Use `git add` before generating patch
+4. **Not testing on clean source**: Always verify with `make clean`
+5. **Leaving reject files**: Remove all `.rej` files after fixing
+
+---
+
+## Example: Complete Patch Update Session
+
+Here's a complete example of updating `0-playwright.patch` from Firefox 144 to Firefox 146:
+
+### 1. Reset and Apply
+
+```bash
+make clean
+cd camoufox-146.0.1-beta.25
+patch -p1 < ../patches/0-playwright.patch
+```
+
+### 2. Find Rejects
+
+```bash
+find . -name '*.rej' -type f
+```
+
+Output shows 20 reject files.
+
+### 3. Analyze First Reject
+
+```bash
+cat dom/base/Navigator.cpp.rej
+```
+
+Shows parameter order changed in `GetAcceptLanguages`.
+
+### 4. Fix the Reject
+
+Search for the function in the actual file, understand the new signature, apply changes manually.
+
+### 5. Repeat for All Rejects
+
+Work through each reject systematically.
+
+### 6. Discover API Change
+
+Firefox 146 refactored mouse events from individual parameters to `SynthesizeMouseEventData` and `SynthesizeMouseEventOptions`. Port all mouse event logic to new API.
+
+### 7. Remove Rejects
+
+```bash
+rm -f dom/base/Navigator.cpp.rej dom/base/Element.cpp.rej ...
+find . -name '*.rej' -type f # Verify empty
+```
+
+### 8. Generate New Patch
+
+```bash
+git diff --binary > /tmp/0-playwright.patch
+cp /tmp/0-playwright.patch ../patches/0-playwright.patch
+```
+
+### 9. Verify
+
+```bash
+cd ..
+make clean
+cd camoufox-146.0.1-beta.25
+patch -p1 < ../patches/0-playwright.patch
+find . -name '*.rej' -type f # Should be empty
+```
+
+---
+
+## Appendix: Firefox Source Navigation
+
+### Finding Files
+
+```bash
+# Find files by name
+find . -name "Navigator.cpp" -type f
+
+# Find files containing a symbol
+grep -r "GetAcceptLanguages" . --include="*.cpp"
+
+# Find class definitions
+grep -r "class Navigator" . --include="*.h"
+```
+
+### Understanding Firefox Code Structure
+
+- `dom/`: DOM implementation
+ - `dom/base/`: Core DOM classes (Window, Document, Navigator, etc.)
+ - `dom/webidl/`: WebIDL interface definitions
+ - `dom/media/webrtc/`: WebRTC implementation
+- `gfx/`: Graphics and font rendering
+ - `gfx/thebes/`: Text rendering (fonts, glyphs, shaping)
+- `layout/`: Layout engine
+ - `layout/generic/`: Text frames
+ - `layout/mathml/`: MathML rendering
+
+### Common Firefox Patterns
+
+**User Context ID Extraction**:
+```cpp
+uint32_t userContextId = 0;
+if (Document* doc = GetDocument()) {
+ if (nsIPrincipal* principal = doc->NodePrincipal()) {
+ auto* bp = mozilla::BasePrincipal::Cast(principal);
+ if (bp) {
+ userContextId = bp->OriginAttributesRef().mUserContextId;
+ }
+ }
+}
+```
+
+**Three-tier userContextId fallback** (for WebIDL functions):
+```cpp
+// 1) Document's principal (preferred)
+if (Document* doc = win->GetDoc()) {
+ if (nsIPrincipal* p = doc->NodePrincipal()) {
+ userContextId = p->OriginAttributesRef().mUserContextId;
+ }
+}
+
+// 2) DocShell origin attributes
+if (userContextId == 0) {
+ if (nsIDocShell* ds = win->GetDocShell()) {
+ auto* concrete = static_cast(ds);
+ userContextId = concrete->GetOriginAttributes().mUserContextId;
+ }
+}
+
+// 3) Top browsing context
+if (userContextId == 0) {
+ if (BrowsingContext* bc = win->GetBrowsingContext()) {
+ RefPtr top = bc->Top();
+ // ... extract from top window
+ }
+}
+```
+
+---
+
+## Summary Checklist
+
+When updating patches for a new Firefox version:
+
+- [ ] Use `make clean` to reset to fresh Firefox source
+- [ ] Apply patch and identify all reject files
+- [ ] Analyze each reject to understand what changed
+- [ ] Search Firefox source for moved/refactored code
+- [ ] Fix rejects by porting logic to new Firefox APIs
+- [ ] Extract userContextId properly using standard patterns
+- [ ] Don't leave TODO comments - fix everything immediately
+- [ ] Remove all `.rej` files after fixing
+- [ ] Add any new files with `git add`
+- [ ] Generate new patch with `git diff --cached --binary` + `git diff --binary`
+- [ ] Verify patch applies cleanly to fresh source
+- [ ] Document any major API changes discovered
+
+---
+
+## Additional Resources
+
+- Firefox source: https://searchfox.org/
+- Firefox API documentation: https://firefox-source-docs.mozilla.org/
+- Mercurial repository: https://hg.mozilla.org/mozilla-central/
+
+---
+
+**Last Updated**: December 2025 (Firefox 146 upgrade)
diff --git a/docs/playwright-maintenance.md b/docs/playwright-maintenance.md
new file mode 100644
index 0000000..a4aeb92
--- /dev/null
+++ b/docs/playwright-maintenance.md
@@ -0,0 +1,165 @@
+# Playwright Maintenance Guide
+
+This document describes how to maintain Playwright integration in Camoufox.
+
+## Overview
+
+Camoufox integrates Playwright's browser automation capabilities through patches and additional files. These need to be kept in sync with upstream Playwright development.
+
+## Patch Files
+
+Location: `patches/playwright/`
+
+| File | Purpose |
+| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `0-playwright.patch` | Playwright's upstream patches. Must be kept up to date with [bootstrap.diff](https://github.com/microsoft/playwright/blob/main/browser_patches/firefox/patches/bootstrap.diff) |
+| `1-leak-fixes.patch` | Undos certain patches from `0-playwright.patch` to fix memory leaks |
+
+## Addition Files
+
+Location: `additions/juggler/`
+
+The `juggler` directory contains Playwright's Juggler protocol implementation. These files must be kept in sync with:
+
+**Upstream Source:** https://github.com/microsoft/playwright/tree/main/browser_patches/firefox/juggler
+
+### Key Files
+
+- **`components/Juggler.js`** - Main Juggler component (legacy JSM format)
+- **`components/Juggler.sys.mjs`** - ESM wrapper for Firefox 146+ compatibility
+- **`components/components.conf`** - XPCOM component registration
+
+### Firefox 146 ESM Migration
+
+Firefox 146 removed JSM (JavaScript Module) support in favor of ESM (ES Modules). To maintain compatibility:
+
+1. **`components.conf`** uses `esModule` field instead of deprecated `jsm` field:
+ ```python
+ {
+ "esModule": "chrome://juggler/content/components/Juggler.sys.mjs",
+ "constructor": "JugglerFactory",
+ }
+ ```
+
+2. **`Juggler.sys.mjs`** acts as an ESM wrapper that imports the legacy JSM file:
+ ```javascript
+ const { JugglerFactory } = ChromeUtils.import(
+ "chrome://juggler/content/components/Juggler.js"
+ );
+ export { JugglerFactory };
+ ```
+
+This maintains backward compatibility while satisfying Firefox 146's static component generator requirements.
+
+## Updating Playwright Integration
+
+### 1. Update Upstream Patches
+
+Compare the current `patches/playwright/0-playwright.patch` with Playwright's [bootstrap.diff](https://github.com/microsoft/playwright/blob/main/browser_patches/firefox/patches/bootstrap.diff).
+
+If changes are needed:
+```bash
+# Download latest bootstrap.diff
+curl -o patches/playwright/0-playwright.patch \
+ https://raw.githubusercontent.com/microsoft/playwright/main/browser_patches/firefox/patches/bootstrap.diff
+
+# Test the build
+make clean && make dir && make build
+```
+
+### 2. Update Juggler Files
+
+Sync `additions/juggler/` with upstream:
+
+```bash
+# Clone Playwright repository
+git clone https://github.com/microsoft/playwright.git /tmp/playwright
+
+# Compare directories
+diff -r additions/juggler/ /tmp/playwright/browser_patches/firefox/juggler/
+
+# Copy updated files (example)
+cp -r /tmp/playwright/browser_patches/firefox/juggler/* additions/juggler/
+
+# IMPORTANT: Preserve Firefox 146 ESM compatibility
+# - Keep additions/juggler/components/Juggler.sys.mjs
+# - Keep additions/juggler/components/components.conf with esModule field
+```
+
+### 3. Verify ESM Wrapper Compatibility
+
+After updating from upstream, ensure the ESM wrapper remains functional:
+
+1. Check that `Juggler.js` still exports `JugglerFactory`:
+ ```javascript
+ var EXPORTED_SYMBOLS = ["Juggler", "JugglerFactory"];
+ var JugglerFactory = function() { /* ... */ };
+ ```
+
+2. If upstream changed the export name, update `Juggler.sys.mjs` accordingly.
+
+3. Verify `components.conf` matches the format above (not upstream's format).
+
+### 4. Test Build
+
+```bash
+# Clean build to verify component registration
+cd camoufox-146.0.1-beta.25
+make clean
+cd ..
+make dir
+cd camoufox-146.0.1-beta.25
+./mach build
+```
+
+**Expected:** No linker errors about `mozCreateComponent`.
+
+**Common Error:** If you see:
+```
+ld64.lld: error: undefined symbol: already_AddRefed mozCreateComponent()
+```
+
+This means `components.conf` is not using ESM format. Fix by ensuring it has:
+- `"esModule"` field (not `"jsm"`)
+- `"constructor": "JugglerFactory"` field (not `"type"`)
+
+## Troubleshooting
+
+### Component Registration Errors
+
+**Error:** `Externally-constructed components may not specify 'constructor' or 'legacy_constructor' properties`
+
+**Cause:** Using `"jsm"` field which is unsupported in Firefox 146.
+
+**Fix:** Use `"esModule"` field instead.
+
+---
+
+**Error:** `Externally-constructed components must specify a type other than nsISupports`
+
+**Cause:** Using external component without proper type specification.
+
+**Fix:** Convert to ESM component with constructor.
+
+---
+
+**Error:** `JavaScript components must specify a constructor`
+
+**Cause:** ESM component missing constructor field.
+
+**Fix:** Add `"constructor": "JugglerFactory"` to components.conf.
+
+### Build Failures
+
+If the build fails after updating Juggler files:
+
+1. Check that all JSM imports in `Juggler.js` are still valid
+2. Verify the ESM wrapper exports match what's imported
+3. Ensure no file paths changed in upstream
+4. Check for Firefox API changes that might require patches
+
+## References
+
+- [Playwright Firefox Patches](https://github.com/microsoft/playwright/tree/main/browser_patches/firefox)
+- [Firefox 146 Component Registration](https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/basics.html)
+- [Firefox ESM Migration Guide](https://firefox-source-docs.mozilla.org/dom/script_loader/index.html)
diff --git a/multibuild.py b/multibuild.py
index e424fae..5ffd640 100644
--- a/multibuild.py
+++ b/multibuild.py
@@ -48,6 +48,11 @@ class BSYS:
"""Bootstrap the build system"""
run('make bootstrap')
+ @staticmethod
+ def generate_assets_car():
+ """Generate Assets.car for macOS builds"""
+ run('make generate-assets-car')
+
def build(self):
"""Build the Camoufox source code"""
os.environ['BUILD_TARGET'] = f'{self.target},{self.arch}'
@@ -119,11 +124,18 @@ def main():
# Clean if requested
if args.clean:
BSYS.clean()
+ # Generate Assets.car if building for macOS
+ if 'macos' in args.target:
+ BSYS.generate_assets_car()
+
+ # Ensure dist directory exists
+ os.makedirs('dist', exist_ok=True)
+
# Run build
for target in args.target:
for arch in args.arch:
if (target, arch) in [("windows", "arm64"), ("macos", "i686")]:
- print(f"Skipping {target} {arch}: Unsuported architecture.")
+ print(f"Skipping {target} {arch}: Unsupported architecture.")
continue
run_build(target, arch)
diff --git a/patches/anti-font-fingerprinting.patch b/patches/anti-font-fingerprinting.patch
index 555dee3..9482bc0 100644
--- a/patches/anti-font-fingerprinting.patch
+++ b/patches/anti-font-fingerprinting.patch
@@ -1,31 +1,839 @@
+diff --git a/dom/base/FontSpacingSeedManager.cpp b/dom/base/FontSpacingSeedManager.cpp
+new file mode 100644
+index 0000000000..fe74a1f432
+--- /dev/null
++++ b/dom/base/FontSpacingSeedManager.cpp
+@@ -0,0 +1,125 @@
++#include "FontSpacingSeedManager.h"
++#include "nsPrintfCString.h"
++#include "nsGlobalWindowInner.h"
++#include "xpcpublic.h"
++#include "mozilla/dom/BrowsingContext.h"
++#include "mozilla/dom/Document.h"
++#include "nsDocShell.h"
++#include "nsPIDOMWindow.h"
++
++namespace mozilla {
++namespace dom {
++
++/* static */ nsString
++FontSpacingSeedManager::KeyForUserContext(uint32_t userContextId) {
++ nsString key;
++ key.AppendLiteral(u"seed_");
++ key.AppendInt(userContextId);
++ return key;
++}
++
++/* static */ nsString
++FontSpacingSeedManager::DisabledKeyForUserContext(uint32_t userContextId) {
++ nsString key;
++ key.AppendLiteral(u"disabled_");
++ key.AppendInt(userContextId);
++ return key;
++}
++
++/* static */ void
++FontSpacingSeedManager::SetSeed(uint32_t userContextId, uint32_t seed) {
++ nsString key = KeyForUserContext(userContextId);
++ RoverfoxStorageManager::PutUint(key, seed);
++
++ // Mark the function as disabled for this context after first use
++ DisableFunction(userContextId);
++}
++
++/* static */ uint32_t
++FontSpacingSeedManager::GetSeed(uint32_t userContextId) {
++ nsString key = KeyForUserContext(userContextId);
++ uint32_t seed = 0;
++ if (RoverfoxStorageManager::GetUint(key, seed)) {
++ return seed;
++ }
++ return 0;
++}
++
++/* static */ bool
++FontSpacingSeedManager::HasSeed(uint32_t userContextId) {
++ nsString key = KeyForUserContext(userContextId);
++ uint32_t seed = 0;
++ return RoverfoxStorageManager::GetUint(key, seed);
++}
++
++/* static */ bool
++FontSpacingSeedManager::IsFunctionDisabled(uint32_t userContextId) {
++ nsString key = DisabledKeyForUserContext(userContextId);
++ bool disabled = false;
++ return RoverfoxStorageManager::GetBool(key, disabled) && disabled;
++}
++
++/* static */ void
++FontSpacingSeedManager::DisableFunction(uint32_t userContextId) {
++ nsString key = DisabledKeyForUserContext(userContextId);
++ RoverfoxStorageManager::PutBool(key, true);
++}
++
++/* static */ bool
++FontSpacingSeedManager::IsFunctionEnabledForWebIDL(JSContext* aCx, JSObject* aObj) {
++ // Get the window from the JS object
++ nsGlobalWindowInner* win = xpc::WindowOrNull(aObj);
++ if (!win) {
++ return false;
++ }
++
++ // Get the user context ID for this window
++ uint32_t userContextId = 0;
++
++ // 1) Prefer the document's principal when available (content contexts)
++ if (Document* doc = win->GetDoc()) {
++ if (nsIPrincipal* p = doc->NodePrincipal()) {
++ userContextId = p->OriginAttributesRef().mUserContextId;
++ }
++ }
++
++ // 2) Fallback: this inner window's docshell origin attributes
++ if (userContextId == 0) {
++ if (nsIDocShell* ds = win->GetDocShell()) {
++ auto* concrete = static_cast(ds);
++ userContextId = concrete->GetOriginAttributes().mUserContextId;
++ }
++ }
++
++ // 3) Fallback: top browsing context's current inner window document/docshell
++ if (userContextId == 0) {
++ if (BrowsingContext* bc = win->GetBrowsingContext()) {
++ RefPtr top = bc->Top();
++ if (top) {
++ if (nsPIDOMWindowOuter* topOuter = top->GetDOMWindow()) {
++ if (nsPIDOMWindowInner* topInner = topOuter->GetCurrentInnerWindow()) {
++ if (Document* topDoc = topInner->GetExtantDoc()) {
++ if (nsIPrincipal* tp = topDoc->NodePrincipal()) {
++ userContextId = tp->OriginAttributesRef().mUserContextId;
++ }
++ }
++ if (userContextId == 0) {
++ if (nsIDocShell* topDS = topInner->GetDocShell()) {
++ auto* concreteTop = static_cast(topDS);
++ userContextId = concreteTop->GetOriginAttributes().mUserContextId;
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++
++ // Check if the function is disabled for this context
++ bool disabled = false;
++ RoverfoxStorageManager::GetBool(DisabledKeyForUserContext(userContextId), disabled);
++ return !disabled;
++}
++
++} // namespace dom
++} // namespace mozilla
+\ No newline at end of file
+diff --git a/dom/base/FontSpacingSeedManager.h b/dom/base/FontSpacingSeedManager.h
+new file mode 100644
+index 0000000000..63d43f3b86
+--- /dev/null
++++ b/dom/base/FontSpacingSeedManager.h
+@@ -0,0 +1,71 @@
++#ifndef mozilla_dom_FontSpacingSeedManager_h
++#define mozilla_dom_FontSpacingSeedManager_h
++
++#include "nsString.h"
++#include "RoverfoxStorageManager.h"
++
++namespace mozilla {
++namespace dom {
++
++/**
++ * FontSpacingSeedManager manages font spacing seeds per user context.
++ * This enables privacy-preserving font fingerprinting by allowing deterministic
++ * font spacing modifications that are isolated by user context.
++ */
++class FontSpacingSeedManager {
++public:
++ /**
++ * Set the font spacing seed for a given user context.
++ * @param userContextId The user context ID (0 for default context)
++ * @param seed The seed value to use for font spacing modifications
++ */
++ static void SetSeed(uint32_t userContextId, uint32_t seed);
++
++ /**
++ * Get the font spacing seed for a given user context.
++ * @param userContextId The user context ID
++ * @return The seed value, or 0 if no seed has been set
++ */
++ static uint32_t GetSeed(uint32_t userContextId);
++
++ /**
++ * Check if a seed has been set for a given user context.
++ * @param userContextId The user context ID
++ * @return true if a seed has been set, false otherwise
++ */
++ static bool HasSeed(uint32_t userContextId);
++
++ /**
++ * Check if the setFontSpacingSeed function has been used and should be disabled for a context.
++ * @param userContextId The user context ID
++ * @return true if the function has been used for this context and should not appear on new windows
++ */
++ static bool IsFunctionDisabled(uint32_t userContextId);
++
++ /**
++ * Mark the setFontSpacingSeed function as used/disabled for a context.
++ * @param userContextId The user context ID
++ */
++ static void DisableFunction(uint32_t userContextId);
++
++ /**
++ * WebIDL-compatible function to check if setFontSpacingSeed should be enabled.
++ * This extracts the user context from the window and checks if disabled.
++ * @param aCx JavaScript context
++ * @param aObj JavaScript object (window)
++ * @return true if function should be available, false otherwise
++ */
++ static bool IsFunctionEnabledForWebIDL(JSContext* aCx, JSObject* aObj);
++
++private:
++ // Helper to convert userContextId to string key
++ static nsString KeyForUserContext(uint32_t userContextId);
++
++ // Helper to create key for tracking function disabled state
++ static nsString DisabledKeyForUserContext(uint32_t userContextId);
++};
++
++} // namespace dom
++} // namespace mozilla
++
++#endif // mozilla_dom_FontSpacingSeedManager_h
+\ No newline at end of file
+diff --git a/dom/base/RoverfoxStorageManager.cpp b/dom/base/RoverfoxStorageManager.cpp
+new file mode 100644
+index 0000000000..f38394d39d
+--- /dev/null
++++ b/dom/base/RoverfoxStorageManager.cpp
+@@ -0,0 +1,58 @@
++#include "RoverfoxStorageManager.h"
++
++namespace mozilla {
++namespace dom {
++
++ThreadSafeKV RoverfoxStorageManager::sUintStorage;
++ThreadSafeStringKV RoverfoxStorageManager::sStringStorage;
++
++/* static */ void
++RoverfoxStorageManager::PutUint(const nsAString& key, uint32_t value)
++{
++ sUintStorage.Put(key, value);
++}
++
++/* static */ bool
++RoverfoxStorageManager::GetUint(const nsAString& key, uint32_t& outValue)
++{
++ return sUintStorage.Get(key, outValue);
++}
++
++/* static */ void
++RoverfoxStorageManager::PutBool(const nsAString& key, bool value)
++{
++ sUintStorage.Put(key, value ? 1 : 0);
++}
++
++/* static */ bool
++RoverfoxStorageManager::GetBool(const nsAString& key, bool& outValue)
++{
++ uint32_t intValue;
++ if (sUintStorage.Get(key, intValue)) {
++ outValue = (intValue != 0);
++ return true;
++ }
++ return false;
++}
++
++/* static */ void
++RoverfoxStorageManager::PutString(const nsAString& key, const nsAString& value)
++{
++ sStringStorage.Put(key, value);
++}
++
++/* static */ bool
++RoverfoxStorageManager::GetString(const nsAString& key, nsAString& outValue)
++{
++ return sStringStorage.Get(key, outValue);
++}
++
++/* static */ void
++RoverfoxStorageManager::Remove(const nsAString& key)
++{
++ sUintStorage.Remove(key);
++ sStringStorage.Remove(key);
++}
++
++} // namespace dom
++} // namespace mozilla
+diff --git a/dom/base/RoverfoxStorageManager.h b/dom/base/RoverfoxStorageManager.h
+new file mode 100644
+index 0000000000..d5700aea62
+--- /dev/null
++++ b/dom/base/RoverfoxStorageManager.h
+@@ -0,0 +1,112 @@
++#ifndef mozilla_dom_RoverfoxStorageManager_h
++#define mozilla_dom_RoverfoxStorageManager_h
++
++#include "mozilla/Mutex.h"
++#include "nsBaseHashtable.h"
++#include "nsString.h"
++#include "nsHashKeys.h"
++
++namespace mozilla {
++namespace dom {
++
++/**
++ * Thread-safe hashtable for storing key-value pairs.
++ * Based on nsClassHashtable with mutex protection for thread safety.
++ */
++class ThreadSafeKV {
++public:
++ ThreadSafeKV()
++ : mMutex("ThreadSafeKV::mMutex") {}
++
++ void Put(const nsAString& key, uint32_t value) {
++ mozilla::MutexAutoLock lock(mMutex);
++ mTable.InsertOrUpdate(key, value);
++ }
++
++ bool Get(const nsAString& key, uint32_t& outValue) {
++ mozilla::MutexAutoLock lock(mMutex);
++ return mTable.Get(key, &outValue);
++ }
++
++ void Remove(const nsAString& key) {
++ mozilla::MutexAutoLock lock(mMutex);
++ mTable.Remove(key);
++ }
++
++private:
++ mozilla::Mutex mMutex;
++ nsTHashMap mTable;
++};
++
++/**
++ * Thread-safe hashtable for storing string key-value pairs.
++ */
++class ThreadSafeStringKV {
++public:
++ ThreadSafeStringKV()
++ : mMutex("ThreadSafeStringKV::mMutex") {}
++
++ void Put(const nsAString& key, const nsAString& value) {
++ mozilla::MutexAutoLock lock(mMutex);
++ mTable.InsertOrUpdate(key, nsString(value));
++ }
++
++ bool Get(const nsAString& key, nsAString& outValue) {
++ mozilla::MutexAutoLock lock(mMutex);
++ nsString value;
++ if (mTable.Get(key, &value)) {
++ outValue = value;
++ return true;
++ }
++ return false;
++ }
++
++ void Remove(const nsAString& key) {
++ mozilla::MutexAutoLock lock(mMutex);
++ mTable.Remove(key);
++ }
++
++private:
++ mozilla::Mutex mMutex;
++ nsTHashMap mTable;
++};
++
++/**
++ * Thread-safe key-value storage manager for whole-browser session scope.
++ * Uses in-memory ThreadSafeKV storage with no SQLite dependencies.
++ */
++class RoverfoxStorageManager {
++public:
++ // Store a uint32_t value under a string key.
++ static void PutUint(const nsAString& key, uint32_t value);
++
++ // Retrieve a uint32_t value. Returns true if present.
++ static bool GetUint(const nsAString& key, uint32_t& outValue);
++
++ // Store a boolean value.
++ static void PutBool(const nsAString& key, bool value);
++
++ // Retrieve a boolean value. Returns true if present.
++ static bool GetBool(const nsAString& key, bool& outValue);
++
++ // Store a UTF-16 string value.
++ static void PutString(const nsAString& key, const nsAString& value);
++
++ // Retrieve a UTF-16 string value. Returns true if present.
++ static bool GetString(const nsAString& key, nsAString& outValue);
++
++ // Remove a key for all supported types (best-effort), no-op if absent.
++ static void Remove(const nsAString& key);
++
++private:
++ // Thread-safe storage for uint32_t values
++ static ThreadSafeKV sUintStorage;
++
++ // Thread-safe storage for string values
++ static ThreadSafeStringKV sStringStorage;
++};
++
++} // namespace dom
++} // namespace mozilla
++
++#endif // mozilla_dom_RoverfoxStorageManager_h
+diff --git a/dom/base/moz.build b/dom/base/moz.build
+index cd9090cda3..609d3b71be 100644
+--- a/dom/base/moz.build
++++ b/dom/base/moz.build
+@@ -191,6 +191,7 @@ EXPORTS.mozilla.dom += [
+ "EventSourceEventService.h",
+ "External.h",
+ "FilteredNodeIterator.h",
++ "FontSpacingSeedManager.h",
+ "FormData.h",
+ "FragmentDirective.h",
+ "FragmentOrElement.h",
+@@ -369,6 +370,7 @@ UNIFIED_SOURCES += [
+ "EventSource.cpp",
+ "EventSourceEventService.cpp",
+ "External.cpp",
++ "FontSpacingSeedManager.cpp",
+ "FormData.cpp",
+ "FragmentDirective.cpp",
+ "FragmentOrElement.cpp",
+diff --git a/dom/base/nsGlobalWindowInner.cpp b/dom/base/nsGlobalWindowInner.cpp
+index 330a707789..fb3ba4d3fb 100644
+--- a/dom/base/nsGlobalWindowInner.cpp
++++ b/dom/base/nsGlobalWindowInner.cpp
+@@ -60,6 +60,7 @@
+ #include "mozilla/EventDispatcher.h"
+ #include "mozilla/EventListenerManager.h"
+ #include "mozilla/EventQueue.h"
++#include "nsIScriptError.h"
+ #include "mozilla/ExtensionPolicyService.h"
+ #include "mozilla/FloatingPoint.h"
+ #include "mozilla/FlushType.h"
+@@ -243,6 +244,9 @@
+ #include "nsICookieService.h"
+ #include "nsID.h"
+ #include "nsIDOMStorageManager.h"
++#include "FontSpacingSeedManager.h"
++#include "nsDocShell.h"
++#include "mozilla/OriginAttributes.h"
+ #include "nsIDOMXULControlElement.h"
+ #include "nsIDeviceSensors.h"
+ #include "nsIDocShell.h"
+@@ -7463,6 +7467,66 @@ IntlUtils* nsGlobalWindowInner::GetIntlUtils(ErrorResult& aError) {
+ return mIntlUtils;
+ }
+
++void nsGlobalWindowInner::SetFontSpacingSeed(uint32_t seed, ErrorResult& aRv) {
++ uint32_t userContextId = 0;
++
++ // 1) Prefer the document's principal when available (content contexts)
++ if (Document* doc = GetDoc()) {
++ if (nsIPrincipal* p = doc->NodePrincipal()) {
++ userContextId = p->OriginAttributesRef().mUserContextId;
++ }
++ }
++
++ // 2) Fallback: this inner window's docshell origin attributes
++ if (userContextId == 0) {
++ if (nsIDocShell* ds = GetDocShell()) {
++ auto* concrete = static_cast(ds);
++ userContextId = concrete->GetOriginAttributes().mUserContextId;
++ }
++ }
++
++ // 3) Fallback: top browsing context's current inner window document/docshell
++ if (userContextId == 0) {
++ if (BrowsingContext* bc = GetBrowsingContext()) {
++ RefPtr top = bc->Top();
++ if (top) {
++ if (nsPIDOMWindowOuter* topOuter = top->GetDOMWindow()) {
++ if (nsPIDOMWindowInner* topInner = topOuter->GetCurrentInnerWindow()) {
++ if (Document* topDoc = topInner->GetExtantDoc()) {
++ if (nsIPrincipal* tp = topDoc->NodePrincipal()) {
++ userContextId = tp->OriginAttributesRef().mUserContextId;
++ }
++ }
++ if (userContextId == 0) {
++ if (nsIDocShell* topDS = topInner->GetDocShell()) {
++ auto* concreteTop = static_cast(topDS);
++ userContextId =
++ concreteTop->GetOriginAttributes().mUserContextId;
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++
++
++ if (userContextId == 0) {
++ aRv.ThrowSecurityError("Unable to resolve user context ID for this context");
++ return;
++ }
++
++ FontSpacingSeedManager::SetSeed(userContextId, seed);
++
++ // Self-destruct: remove this function from the window object after first use
++ if (JSContext* cx = nsContentUtils::GetCurrentJSContext()) {
++ JS::Rooted global(cx, JS::CurrentGlobalOrNull(cx));
++ if (global) {
++ JS_DeleteProperty(cx, global, "setFontSpacingSeed");
++ }
++ }
++}
++
+ void nsGlobalWindowInner::StoreSharedWorker(SharedWorker* aSharedWorker) {
+ MOZ_ASSERT(aSharedWorker);
+ MOZ_ASSERT(!mSharedWorkers.Contains(aSharedWorker));
+diff --git a/dom/base/nsGlobalWindowInner.h b/dom/base/nsGlobalWindowInner.h
+index 648bcda859..46cb7d6f4a 100644
+--- a/dom/base/nsGlobalWindowInner.h
++++ b/dom/base/nsGlobalWindowInner.h
+@@ -681,6 +681,9 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget,
+
+ mozilla::dom::IntlUtils* GetIntlUtils(mozilla::ErrorResult& aRv);
+
++ // Font spacing seed for privacy-preserving font fingerprinting
++ void SetFontSpacingSeed(uint32_t seed, mozilla::ErrorResult& aRv);
++
+ void StoreSharedWorker(mozilla::dom::SharedWorker* aSharedWorker);
+
+ void ForgetSharedWorker(mozilla::dom::SharedWorker* aSharedWorker);
+diff --git a/dom/canvas/CanvasRenderingContext2D.cpp b/dom/canvas/CanvasRenderingContext2D.cpp
+index 989dba05ca..14f8be6dcd 100644
+--- a/dom/canvas/CanvasRenderingContext2D.cpp
++++ b/dom/canvas/CanvasRenderingContext2D.cpp
+@@ -57,6 +57,7 @@
+ #include "mozilla/dom/CanvasRenderingContext2DBinding.h"
+ #include "mozilla/dom/DOMMatrix.h"
+ #include "mozilla/dom/Document.h"
++#include "mozilla/OriginAttributes.h"
+ #include "mozilla/dom/FontFaceSet.h"
+ #include "mozilla/dom/FontFaceSetImpl.h"
+ #include "mozilla/dom/GeneratePlaceholderCanvasData.h"
+@@ -4719,10 +4720,20 @@ struct MOZ_STACK_CLASS CanvasBidiProcessor final
+ } else {
+ flags &= ~gfx::ShapedTextFlags::TEXT_IS_RTL;
+ }
++ // Extract user context ID from Canvas element's document
++ uint32_t userContextId = 0;
++ if (mCtx && mCtx->mCanvasElement) {
++ if (Document* doc = mCtx->mCanvasElement->GetOwnerDocument()) {
++ if (nsIPrincipal* principal = doc->NodePrincipal()) {
++ const OriginAttributes& attrs = principal->OriginAttributesRef();
++ userContextId = attrs.mUserContextId;
++ }
++ }
++ }
+ mTextRun = mFontgrp->MakeTextRun(
+ aText, aLength, mDrawTarget, mAppUnitsPerDevPixel, flags,
+ nsTextFrameUtils::Flags::DontSkipDrawingForPendingUserFonts,
+- mMissingFonts.get());
++ mMissingFonts.get(), userContextId);
+ pfl->Unlock();
+ }
+
+diff --git a/dom/canvas/OffscreenCanvas.cpp b/dom/canvas/OffscreenCanvas.cpp
+index e32eeaeeab..fed027072c 100644
+--- a/dom/canvas/OffscreenCanvas.cpp
++++ b/dom/canvas/OffscreenCanvas.cpp
+@@ -635,6 +635,15 @@ void OffscreenCanvas::ReportBlockedFontFamily(const nsCString& aMsg) const {
+ }
+ }
+
++mozilla::dom::Document* OffscreenCanvas::GetDocument() const {
++ if (NS_IsMainThread()) {
++ if (nsCOMPtr win = do_QueryInterface(GetOwnerGlobal())) {
++ return win->GetExtantDoc();
++ }
++ }
++ return nullptr;
++}
++
+ bool OffscreenCanvas::IsChrome() const {
+ if (NS_IsMainThread()) {
+ nsCOMPtr win = do_QueryInterface(GetOwnerGlobal());
+diff --git a/dom/webidl/Window.webidl b/dom/webidl/Window.webidl
+index 7c509eb87e..7a076768d0 100644
+--- a/dom/webidl/Window.webidl
++++ b/dom/webidl/Window.webidl
+@@ -892,6 +892,12 @@ partial interface Window {
+ readonly attribute VisualViewport visualViewport;
+ };
+
++// Font spacing seed interface for privacy-preserving font fingerprinting
++partial interface Window {
++ [Throws, Func="mozilla::dom::FontSpacingSeedManager::IsFunctionEnabledForWebIDL"]
++ undefined setFontSpacingSeed(unsigned long seed);
++};
++
+ // Used to assign marks to appear on the scrollbar when
+ // finding on a page.
+ partial interface Window {
+diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h
+index c5d17c1c0a..216bdace64 100644
+--- a/dom/workers/WorkerPrivate.h
++++ b/dom/workers/WorkerPrivate.h
+@@ -1103,8 +1103,6 @@ class WorkerPrivate final
+
+ void WorkerScriptLoaded();
+
+- Document* GetDocument() const;
+-
+ void MemoryPressure();
+
+ void UpdateContextOptions(const JS::ContextOptions& aContextOptions);
+diff --git a/gfx/src/nsFontMetrics.cpp b/gfx/src/nsFontMetrics.cpp
+index ef081ebe40..a921c49f43 100644
+--- a/gfx/src/nsFontMetrics.cpp
++++ b/gfx/src/nsFontMetrics.cpp
+@@ -23,9 +23,12 @@
+ #include "nsStyleConsts.h" // for StyleHyphens::None
+ #include "mozilla/Assertions.h" // for MOZ_ASSERT
+ #include "mozilla/UniquePtr.h" // for UniquePtr
++#include "mozilla/dom/Document.h" // for Document
++#include "mozilla/OriginAttributes.h" // for OriginAttributes
+
+ class gfxUserFontSet;
+ using namespace mozilla;
++using namespace mozilla::dom;
+
+ namespace {
+
+@@ -35,17 +38,37 @@ class AutoTextRun {
+
+ AutoTextRun(const nsFontMetrics* aMetrics, DrawTarget* aDrawTarget,
+ const char* aString, uint32_t aLength) {
++ // Extract user context ID from PresContext document if available
++ uint32_t userContextId = 0;
++ if (aMetrics->mPresContext) {
++ if (Document* doc = aMetrics->mPresContext->Document()) {
++ if (nsIPrincipal* principal = doc->NodePrincipal()) {
++ const OriginAttributes& attrs = principal->OriginAttributesRef();
++ userContextId = attrs.mUserContextId;
++ }
++ }
++ }
+ mTextRun = aMetrics->GetThebesFontGroup()->MakeTextRun(
+ reinterpret_cast(aString), aLength, aDrawTarget,
+ aMetrics->AppUnitsPerDevPixel(), ComputeFlags(aMetrics),
+- nsTextFrameUtils::Flags(), nullptr);
++ nsTextFrameUtils::Flags(), nullptr, userContextId);
+ }
+
+ AutoTextRun(const nsFontMetrics* aMetrics, DrawTarget* aDrawTarget,
+ const char16_t* aString, uint32_t aLength) {
++ // Extract user context ID from PresContext document if available
++ uint32_t userContextId = 0;
++ if (aMetrics->mPresContext) {
++ if (Document* doc = aMetrics->mPresContext->Document()) {
++ if (nsIPrincipal* principal = doc->NodePrincipal()) {
++ const OriginAttributes& attrs = principal->OriginAttributesRef();
++ userContextId = attrs.mUserContextId;
++ }
++ }
++ }
+ mTextRun = aMetrics->GetThebesFontGroup()->MakeTextRun(
+ aString, aLength, aDrawTarget, aMetrics->AppUnitsPerDevPixel(),
+- ComputeFlags(aMetrics), nsTextFrameUtils::Flags(), nullptr);
++ ComputeFlags(aMetrics), nsTextFrameUtils::Flags(), nullptr, userContextId);
+ }
+
+ gfxTextRun* get() const { return mTextRun.get(); }
+diff --git a/gfx/src/nsFontMetrics.h b/gfx/src/nsFontMetrics.h
+index cc8aec7cc7..e4e51f5000 100644
+--- a/gfx/src/nsFontMetrics.h
++++ b/gfx/src/nsFontMetrics.h
+@@ -257,6 +257,10 @@ class nsFontMetrics final {
+ bool AllowForceGDIClassic() const { return mAllowForceGDIClassic; }
+ #endif
+
++ // Pointer to the pres context for which this fontMetrics object was
++ // created.
++ nsPresContext* MOZ_NON_OWNING_REF mPresContext;
++
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~nsFontMetrics();
+@@ -264,9 +268,6 @@ class nsFontMetrics final {
+ const nsFont mFont;
+ RefPtr mFontGroup;
+ RefPtr const mLanguage;
+- // Pointer to the pres context for which this fontMetrics object was
+- // created.
+- nsPresContext* MOZ_NON_OWNING_REF mPresContext;
+ const int32_t mP2A;
+
+ // The font orientation (horizontal or vertical) for which these metrics
+diff --git a/gfx/thebes/gfxFont.cpp b/gfx/thebes/gfxFont.cpp
+index 6f02e3a566..ca3b60d550 100644
+--- a/gfx/thebes/gfxFont.cpp
++++ b/gfx/thebes/gfxFont.cpp
+@@ -20,6 +20,7 @@
+
+ #include "gfxGlyphExtents.h"
+ #include "gfxPlatform.h"
++#include "mozilla/dom/FontSpacingSeedManager.h"
+ #include "gfxTextRun.h"
+ #include "nsGkAtoms.h"
+
+@@ -47,6 +48,7 @@
+ #include "gfxSVGGlyphs.h"
+ #include "gfx2DGlue.h"
+ #include "TextDrawTarget.h"
++#include "mozilla/dom/FontSpacingSeedManager.h"
+
+ #include "ThebesRLBox.h"
+
+@@ -3284,7 +3286,7 @@ bool gfxFont::ProcessShapedWordInternal(
+ Script aRunScript, nsAtom* aLanguage, bool aVertical,
+ int32_t aAppUnitsPerDevUnit, gfx::ShapedTextFlags aFlags,
+ RoundingFlags aRounding, gfxTextPerfMetrics* aTextPerf GFX_MAYBE_UNUSED,
+- Func aCallback) {
++ uint32_t aUserContextId, Func aCallback) {
+ WordCacheKey key(aText, aLength, aHash, aRunScript, aLanguage,
+ aAppUnitsPerDevUnit, aFlags, aRounding);
+ {
+@@ -3317,6 +3319,8 @@ bool gfxFont::ProcessShapedWordInternal(
+ NS_WARNING("failed to create gfxShapedWord - expect missing text");
+ return false;
+ }
++ // Propagate user context ID for HarfBuzz shaping/logging.
++ newShapedWord->SetUserContextId(aUserContextId);
+ DebugOnly ok =
+ ShapeText(aDrawTarget, aText, 0, aLength, aRunScript, aLanguage,
+ aVertical, aRounding, newShapedWord.get());
+@@ -3420,7 +3424,7 @@ bool gfxFont::ProcessSingleSpaceShapedWord(
+ return ProcessShapedWordInternal(
+ aDrawTarget, &space, 1, gfxShapedWord::HashMix(0, ' '), Script::LATIN,
+ /* aLanguage = */ nullptr, aVertical, aAppUnitsPerDevUnit, aFlags,
+- aRounding, nullptr, aCallback);
++ aRounding, nullptr, 0 /* pbid */, aCallback);
+ }
+
+ bool gfxFont::ShapeText(DrawTarget* aDrawTarget, const uint8_t* aText,
+@@ -3778,6 +3782,7 @@ bool gfxFont::SplitAndInitTextRun(
+ bool processed = ProcessShapedWordInternal(
+ aDrawTarget, aString + wordStart, length, hash, aRunScript, aLanguage,
+ vertical, appUnitsPerDevUnit, wordFlags, rounding, tp,
++ aTextRun->GetUserContextId(),
+ [&](gfxShapedWord* aShapedWord) {
+ aTextRun->CopyGlyphDataFrom(aShapedWord, aRunStart + wordStart);
+ });
+@@ -3803,6 +3808,7 @@ bool gfxFont::SplitAndInitTextRun(
+ aDrawTarget, &boundary, 1, gfxShapedWord::HashMix(0, boundary),
+ aRunScript, aLanguage, vertical, appUnitsPerDevUnit,
+ flags | gfx::ShapedTextFlags::TEXT_IS_8BIT, rounding, tp,
++ aTextRun->GetUserContextId(),
+ [&](gfxShapedWord* aShapedWord) {
+ aTextRun->CopyGlyphDataFrom(aShapedWord, aRunStart + i);
+ if (boundary == ' ') {
+diff --git a/gfx/thebes/gfxFont.h b/gfx/thebes/gfxFont.h
+index 4601fc3f17..78c2f1116c 100644
+--- a/gfx/thebes/gfxFont.h
++++ b/gfx/thebes/gfxFont.h
+@@ -737,6 +737,10 @@ class gfxShapedText {
+
+ virtual ~gfxShapedText() = default;
+
++ // Optional accessor overridden by gfxTextRun to expose the private browsing ID.
++ // Default returns 0 for non-textrun shaped text objects.
++ virtual uint32_t GetUserContextId() const { return 0; }
++
+ /**
+ * This class records the information associated with a character in the
+ * input string. It's optimized for the case where there is one glyph
+@@ -1326,6 +1330,11 @@ class gfxShapedWord final : public gfxShapedText {
+ // allocated via malloc.
+ void operator delete(void* p) { free(p); }
+
++ // User Context ID plumbing for shaping-time access.
++ // HarfBuzz shaper will query this via gfxShapedText::GetUserContextId().
++ void SetUserContextId(uint32_t aId) { mUserContextId = aId; }
++ uint32_t GetUserContextId() const override { return mUserContextId; }
++
+ const CompressedGlyph* GetCharacterGlyphs() const override {
+ return &mCharGlyphsStorage[0];
+ }
+@@ -1409,6 +1418,9 @@ class gfxShapedWord final : public gfxShapedText {
+ // With multithreaded shaping, this may be updated by any thread.
+ std::atomic mAgeCounter;
+
++ // User Context ID carried with the shaped word for shaper access.
++ uint32_t mUserContextId = 0;
++
+ // The mCharGlyphsStorage array is actually a variable-size member;
+ // when the ShapedWord is created, its size will be increased as necessary
+ // to allow the proper number of glyphs to be stored.
+@@ -2091,12 +2103,14 @@ class gfxFont {
+ // for use in setting up a gfxTextRun.
+ template
+ bool ProcessShapedWordInternal(DrawTarget* aDrawTarget, const T* aText,
+- uint32_t aLength, uint32_t aHash,
+- Script aRunScript, nsAtom* aLanguage,
+- bool aVertical, int32_t aAppUnitsPerDevUnit,
+- mozilla::gfx::ShapedTextFlags aFlags,
+- RoundingFlags aRounding,
+- gfxTextPerfMetrics* aTextPerf, Func aCallback);
++ uint32_t aLength, uint32_t aHash,
++ Script aRunScript, nsAtom* aLanguage,
++ bool aVertical, int32_t aAppUnitsPerDevUnit,
++ mozilla::gfx::ShapedTextFlags aFlags,
++ RoundingFlags aRounding,
++ gfxTextPerfMetrics* aTextPerf,
++ uint32_t aUserContextId,
++ Func aCallback);
+
+ // whether a given feature is included in feature settings from both the
+ // font and the style. aFeatureOn set if resolved feature value is non-zero
diff --git a/gfx/thebes/gfxHarfBuzzShaper.cpp b/gfx/thebes/gfxHarfBuzzShaper.cpp
-index 18bc08a6bf..46e0675aa2 100644
+index 1a5bdaba99..46226fc88d 100644
--- a/gfx/thebes/gfxHarfBuzzShaper.cpp
+++ b/gfx/thebes/gfxHarfBuzzShaper.cpp
-@@ -17,6 +17,9 @@
+@@ -17,6 +17,10 @@
#include "harfbuzz/hb.h"
#include "harfbuzz/hb-ot.h"
+#include
+#include
+#include "MaskConfig.hpp"
++#include "mozilla/dom/FontSpacingSeedManager.h"
#include
-@@ -1557,6 +1560,36 @@ bool gfxHarfBuzzShaper::ShapeText(DrawTarget* aDrawTarget,
+@@ -1567,6 +1571,41 @@ bool gfxHarfBuzzShaper::ShapeText(DrawTarget* aDrawTarget,
hb_shape(mHBFont, mBuffer, features.Elements(), features.Length());
-+ static uint32_t seed;
-+ if (auto value = MaskConfig::GetUint32("fonts:spacing_seed"))
-+ seed = value.value();
-+ else
-+ seed = static_cast(
-+ std::chrono::high_resolution_clock::now().time_since_epoch().count());
++ // Resolve seed from manager using user context ID, with deterministic fallback.
++ uint32_t pbid = aShapedText ? aShapedText->GetUserContextId() : 0;
++ uint32_t seed = mozilla::dom::FontSpacingSeedManager::GetSeed(pbid);
++ bool seedFromManager = (seed != 0);
++ if (!seedFromManager) {
++ seed = 0x6D2B79F5u; // fixed constant to avoid time-based variance
++ }
++ printf("HarfBuzzShaper: pbid=%u seed=%u (from_manager=%d)\n",
++ pbid,
++ seed,
++ seedFromManager ? 1 : 0);
+
+ // Generate a random float [0, 0.1] to offset the letter spacing
+ seed = (seed * 1103515245 + 12345) & 0x7fffffff;
-+ float randomFloat = (static_cast(seed) / 0x7fffffff) * 0.1f;
++ float randomFloat = (static_cast(seed) / 2147483647.0f) * 0.1f;
+ hb_position_t spacing = FloatToFixed(randomFloat);
+
+ uint32_t glyphCount;
@@ -49,3 +857,523 @@ index 18bc08a6bf..46e0675aa2 100644
if (isRightToLeft) {
hb_buffer_reverse(mBuffer);
}
+@@ -1872,3 +1911,4 @@ nsresult gfxHarfBuzzShaper::SetGlyphsFromRun(gfxShapedText* aShapedText,
+
+ return NS_OK;
+ }
++
+diff --git a/gfx/thebes/gfxTextRun.cpp b/gfx/thebes/gfxTextRun.cpp
+index de9a9661a1..f878bdb36c 100644
+--- a/gfx/thebes/gfxTextRun.cpp
++++ b/gfx/thebes/gfxTextRun.cpp
+@@ -25,6 +25,7 @@
+ #include "mozilla/Likely.h"
+ #include "mozilla/MruCache.h"
+ #include "mozilla/ServoStyleSet.h"
++#include "mozilla/ServoStyleSetInlines.h"
+ #include "mozilla/Sprintf.h"
+ #include "mozilla/StaticPresData.h"
+ #include "mozilla/UniquePtr.h"
+@@ -34,6 +35,7 @@
+ #include "nsUnicodeProperties.h"
+ #include "SharedFontList-impl.h"
+ #include "TextDrawTarget.h"
++#include "mozilla/BasePrincipal.h"
+
+ #ifdef XP_WIN
+ # include "gfxWindowsPlatform.h"
+@@ -145,25 +147,26 @@ void* gfxTextRun::AllocateStorageForTextRun(size_t aSize, uint32_t aLength) {
+ already_AddRefed gfxTextRun::Create(
+ const gfxTextRunFactory::Parameters* aParams, uint32_t aLength,
+ gfxFontGroup* aFontGroup, gfx::ShapedTextFlags aFlags,
+- nsTextFrameUtils::Flags aFlags2) {
++ nsTextFrameUtils::Flags aFlags2, uint32_t aUserContextId) {
+ void* storage = AllocateStorageForTextRun(sizeof(gfxTextRun), aLength);
+ if (!storage) {
+ return nullptr;
+ }
+
+ RefPtr result =
+- new (storage) gfxTextRun(aParams, aLength, aFontGroup, aFlags, aFlags2);
++ new (storage) gfxTextRun(aParams, aLength, aFontGroup, aFlags, aFlags2, aUserContextId);
+ return result.forget();
+ }
+
+ gfxTextRun::gfxTextRun(const gfxTextRunFactory::Parameters* aParams,
+ uint32_t aLength, gfxFontGroup* aFontGroup,
+ gfx::ShapedTextFlags aFlags,
+- nsTextFrameUtils::Flags aFlags2)
++ nsTextFrameUtils::Flags aFlags2, uint32_t aUserContextId)
+ : gfxShapedText(aLength, aFlags, aParams->mAppUnitsPerDevUnit),
+ mUserData(aParams->mUserData),
+ mFontGroup(aFontGroup),
+ mFlags2(aFlags2),
++ mUserContextId(aUserContextId),
+ mReleasedFontGroup(false),
+ mReleasedFontGroupSkippedDrawing(false),
+ mShapingState(eShapingState_Normal) {
+@@ -728,7 +731,8 @@ void gfxTextRun::DrawEmphasisMarks(
+ gfxContext* aContext, gfxTextRun* aMark, gfxFloat aMarkAdvance,
+ gfx::Point aPt, Range aRange, const PropertyProvider* aProvider,
+ mozilla::gfx::PaletteCache& aPaletteCache) const {
+- MOZ_ASSERT(aRange.end <= GetLength());
++ MOZ_ASSERT(
++ aRange.end <= GetLength());
+
+ EmphasisMarkDrawParams params(aContext, aPaletteCache);
+ params.mark = aMark;
+@@ -1870,6 +1874,19 @@ gfxFontGroup::gfxFontGroup(FontVisibilityProvider* aFontVisibilityProvider,
+ mFontVariantEmoji(aVariantEmoji) {
+ // We don't use SetUserFontSet() here, as we want to unconditionally call
+ // EnsureFontList() rather than only do UpdateUserFonts() if it changed.
++
++ // Initialize cached user context ID from the font visibility provider if available.
++ mUserContextId = 0;
++ if (mFontVisibilityProvider) {
++ if (auto* doc = mFontVisibilityProvider->GetDocument()) {
++ if (auto* principal = doc->NodePrincipal()) {
++ auto* bp = mozilla::BasePrincipal::Cast(principal);
++ if (bp) {
++ mUserContextId = bp->OriginAttributesRef().mUserContextId;
++ }
++ }
++ }
++ }
+ }
+
+ gfxFontGroup::~gfxFontGroup() {
+@@ -2485,12 +2502,12 @@ already_AddRefed gfxFontGroup::MakeHyphenTextRun(
+ RefPtr font = GetFirstValidFont(uint32_t(hyphen));
+ if (font->HasCharacter(hyphen)) {
+ return MakeTextRun(&hyphen, 1, aDrawTarget, aAppUnitsPerDevUnit, aFlags,
+- nsTextFrameUtils::Flags(), nullptr);
++ nsTextFrameUtils::Flags(), nullptr, mUserContextId);
+ }
+
+ static const uint8_t dash = '-';
+ return MakeTextRun(&dash, 1, aDrawTarget, aAppUnitsPerDevUnit, aFlags,
+- nsTextFrameUtils::Flags(), nullptr);
++ nsTextFrameUtils::Flags(), nullptr, mUserContextId);
+ }
+
+ gfxFloat gfxFontGroup::GetHyphenWidth(
+@@ -2511,7 +2528,7 @@ template
+ already_AddRefed gfxFontGroup::MakeTextRun(
+ const T* aString, uint32_t aLength, const Parameters* aParams,
+ gfx::ShapedTextFlags aFlags, nsTextFrameUtils::Flags aFlags2,
+- gfxMissingFontRecorder* aMFR) {
++ gfxMissingFontRecorder* aMFR, uint32_t aUserContextId) {
+ if (aLength == 0) {
+ return MakeEmptyTextRun(aParams, aFlags, aFlags2);
+ }
+@@ -2530,8 +2547,16 @@ already_AddRefed gfxFontGroup::MakeTextRun(
+ return MakeBlankTextRun(aString, aLength, aParams, aFlags, aFlags2);
+ }
+
++ // If caller didn't provide an ID, default to the group's cached value.
++ if (aUserContextId == 0) {
++ aUserContextId = mUserContextId;
++ }
++
++ // Log the user context id used for this text run creation
++ printf("MakeTextRun: userContextId=%u\n", aUserContextId);
++
+ RefPtr textRun =
+- gfxTextRun::Create(aParams, aLength, this, aFlags, aFlags2);
++ gfxTextRun::Create(aParams, aLength, this, aFlags, aFlags2, aUserContextId);
+ if (!textRun) {
+ return nullptr;
+ }
+@@ -2547,11 +2572,11 @@ already_AddRefed gfxFontGroup::MakeTextRun(
+ template already_AddRefed gfxFontGroup::MakeTextRun(
+ const uint8_t* aString, uint32_t aLength, const Parameters* aParams,
+ gfx::ShapedTextFlags aFlags, nsTextFrameUtils::Flags aFlags2,
+- gfxMissingFontRecorder* aMFR);
++ gfxMissingFontRecorder* aMFR, uint32_t aUserContextId);
+ template already_AddRefed gfxFontGroup::MakeTextRun(
+ const char16_t* aString, uint32_t aLength, const Parameters* aParams,
+ gfx::ShapedTextFlags aFlags, nsTextFrameUtils::Flags aFlags2,
+- gfxMissingFontRecorder* aMFR);
++ gfxMissingFontRecorder* aMFR, uint32_t aUserContextId);
+
+ // Helper to get a hashtable that maps tags to Script codes, created on first
+ // use.
+@@ -2609,8 +2634,9 @@ static Script ResolveScriptForLang(const nsAtom* aLanguage, Script aDefault) {
+ static LangScriptCache sCache;
+ static RWLock sLock("LangScriptCache lock");
+
+- MOZ_ASSERT(aDefault != Script::INVALID &&
+- aDefault < Script::NUM_SCRIPT_CODES);
++ MOZ_ASSERT(
++ aDefault != Script::INVALID &&
++ aDefault < Script::NUM_SCRIPT_CODES);
+
+ {
+ // Try to use a cached value without taking an exclusive lock.
+@@ -3045,7 +3071,7 @@ gfxTextRun* gfxFontGroup::GetEllipsisTextRun(
+ nullptr, 0, aAppUnitsPerDevPixel};
+ mCachedEllipsisTextRun =
+ MakeTextRun(ellipsis.BeginReading(), ellipsis.Length(), ¶ms, aFlags,
+- nsTextFrameUtils::Flags(), nullptr);
++ nsTextFrameUtils::Flags(), nullptr, mUserContextId);
+ if (!mCachedEllipsisTextRun) {
+ return nullptr;
+ }
+@@ -3460,7 +3486,7 @@ already_AddRefed gfxFontGroup::FindFontForChar(
+ font = FindFallbackFaceForChar(ff, aCh, aNextCh, presentation);
+ if (font) {
+ if (CheckCandidate(font,
+- {FontMatchType::Kind::kFontGroup, ff.Generic()})) {
++ {FontMatchType::Kind::kFontGroup, mFonts[i].Generic()})) {
+ return font.forget();
+ }
+ }
+diff --git a/gfx/thebes/gfxTextRun.h b/gfx/thebes/gfxTextRun.h
+index 06ccaa6082..556ab66718 100644
+--- a/gfx/thebes/gfxTextRun.h
++++ b/gfx/thebes/gfxTextRun.h
+@@ -487,13 +487,14 @@ class gfxTextRun : public gfxShapedText {
+ void ClearFlagBits(nsTextFrameUtils::Flags aFlags) { mFlags2 &= ~aFlags; }
+ const gfxSkipChars& GetSkipChars() const { return mSkipChars; }
+ gfxFontGroup* GetFontGroup() const { return mFontGroup; }
++ uint32_t GetUserContextId() const override { return mUserContextId; }
+
+ // Call this, don't call "new gfxTextRun" directly. This does custom
+ // allocation and initialization
+ static already_AddRefed Create(
+ const gfxTextRunFactory::Parameters* aParams, uint32_t aLength,
+ gfxFontGroup* aFontGroup, mozilla::gfx::ShapedTextFlags aFlags,
+- nsTextFrameUtils::Flags aFlags2);
++ nsTextFrameUtils::Flags aFlags2, uint32_t aUserContextId = 0);
+
+ // The text is divided into GlyphRuns as necessary. (In the vast majority
+ // of cases, a gfxTextRun contains just a single GlyphRun.)
+@@ -802,7 +803,7 @@ class gfxTextRun : public gfxShapedText {
+ */
+ gfxTextRun(const gfxTextRunFactory::Parameters* aParams, uint32_t aLength,
+ gfxFontGroup* aFontGroup, mozilla::gfx::ShapedTextFlags aFlags,
+- nsTextFrameUtils::Flags aFlags2);
++ nsTextFrameUtils::Flags aFlags2, uint32_t aUserContextId = 0);
+
+ // Whether we need to fetch actual glyph extents from the fonts.
+ bool NeedsGlyphExtents() const;
+@@ -894,6 +895,8 @@ class gfxTextRun : public gfxShapedText {
+ nsTextFrameUtils::Flags
+ mFlags2; // additional flags (see also gfxShapedText::mFlags)
+
++ uint32_t mUserContextId; // user context ID for font spacing seed
++
+ bool mDontSkipDrawing; // true if the text run must not skip drawing, even if
+ // waiting for a user font download, e.g. because we
+ // are using it to draw canvas text
+@@ -974,7 +977,8 @@ class gfxFontGroup final : public gfxTextRunFactory {
+ const Parameters* aParams,
+ mozilla::gfx::ShapedTextFlags aFlags,
+ nsTextFrameUtils::Flags aFlags2,
+- gfxMissingFontRecorder* aMFR);
++ gfxMissingFontRecorder* aMFR,
++ uint32_t aUserContextId = 0);
+
+ /**
+ * Textrun creation helper for clients that don't want to pass
+@@ -986,10 +990,11 @@ class gfxFontGroup final : public gfxTextRunFactory {
+ int32_t aAppUnitsPerDevUnit,
+ mozilla::gfx::ShapedTextFlags aFlags,
+ nsTextFrameUtils::Flags aFlags2,
+- gfxMissingFontRecorder* aMFR) {
++ gfxMissingFontRecorder* aMFR,
++ uint32_t aUserContextId = 0) {
+ gfxTextRunFactory::Parameters params = {
+ aRefDrawTarget, nullptr, nullptr, nullptr, 0, aAppUnitsPerDevUnit};
+- return MakeTextRun(aString, aLength, ¶ms, aFlags, aFlags2, aMFR);
++ return MakeTextRun(aString, aLength, ¶ms, aFlags, aFlags2, aMFR, aUserContextId);
+ }
+
+ // Get the (possibly-cached) width of the hyphen character.
+@@ -1417,6 +1422,8 @@ class gfxFontGroup final : public gfxTextRunFactory {
+ uint32_t mFontListGeneration = 0; // platform font list generation for this
+ // fontgroup
+
++ uint32_t mUserContextId = 0; // user context ID for font spacing seed
++
+ /**
+ * Textrun creation short-cuts for special cases where we don't need to
+ * call a font shaper to generate glyphs.
+diff --git a/layout/base/nsLayoutUtils.cpp b/layout/base/nsLayoutUtils.cpp
+index 89ed5fec3e..32920e9b71 100644
+--- a/layout/base/nsLayoutUtils.cpp
++++ b/layout/base/nsLayoutUtils.cpp
+@@ -144,7 +144,9 @@
+ #include "nsIFrameInlines.h"
+ #include "nsIImageLoadingContent.h"
+ #include "nsIInterfaceRequestorUtils.h"
++#include "nsILoadContext.h"
+ #include "nsIWidget.h"
++#include "mozilla/OriginAttributes.h"
+ #include "nsListControlFrame.h"
+ #include "nsMenuPopupFrame.h"
+ #include "nsPIDOMWindow.h"
+@@ -1210,6 +1212,33 @@ nsIFrame* nsLayoutUtils::GetLastSibling(nsIFrame* aFrame) {
+ return aFrame;
+ }
+
++/* static */ uint32_t
++nsLayoutUtils::GetUserContextId(nsIFrame* aFrame)
++{
++ if (!aFrame || !aFrame->GetContent()) {
++ return 0;
++ }
++
++ mozilla::dom::Document* doc = aFrame->GetContent()->GetComposedDoc();
++ if (!doc) {
++ return 0;
++ }
++
++ nsIDocShell* docShell = doc->GetDocShell();
++ if (!docShell) {
++ return 0;
++ }
++
++ nsCOMPtr loadContext = do_QueryInterface(docShell);
++ if (!loadContext) {
++ return 0;
++ }
++
++ mozilla::OriginAttributes attrs;
++ loadContext->GetOriginAttributes(attrs);
++ return attrs.mUserContextId;
++}
++
+ // static
+ nsView* nsLayoutUtils::FindSiblingViewFor(nsView* aParentView,
+ nsIFrame* aFrame) {
+diff --git a/layout/base/nsLayoutUtils.h b/layout/base/nsLayoutUtils.h
+index 6c7d485e38..da404c0e5c 100644
+--- a/layout/base/nsLayoutUtils.h
++++ b/layout/base/nsLayoutUtils.h
+@@ -423,6 +423,11 @@ class nsLayoutUtils {
+ */
+ static nsIFrame* GetLastSibling(nsIFrame* aFrame);
+
++ /**
++ * Get the user context ID from a frame's document context
++ */
++ static uint32_t GetUserContextId(nsIFrame* aFrame);
++
+ /**
+ * FindSiblingViewFor locates the child of aParentView that aFrame's
+ * view should be inserted 'above' (i.e., before in sibling view
+diff --git a/layout/base/nsPresContext.cpp b/layout/base/nsPresContext.cpp
+index 9bb509ed5f..a5e21d2f57 100644
+--- a/layout/base/nsPresContext.cpp
++++ b/layout/base/nsPresContext.cpp
+@@ -425,6 +425,10 @@ bool nsPresContext::IsChrome() const {
+ return Document()->IsInChromeDocShell();
+ }
+
++mozilla::dom::Document* nsPresContext::GetDocument() const {
++ return Document();
++}
++
+ void nsPresContext::GetUserPreferences() {
+ if (!GetPresShell()) {
+ // No presshell means nothing to do here. We'll do this when we
+diff --git a/layout/generic/MathMLTextRunFactory.cpp b/layout/generic/MathMLTextRunFactory.cpp
+index 13c9cc5c11..b940be8790 100644
+--- a/layout/generic/MathMLTextRunFactory.cpp
++++ b/layout/generic/MathMLTextRunFactory.cpp
+@@ -11,6 +11,7 @@
+ #include "mozilla/ComputedStyleInlines.h"
+ #include "mozilla/StaticPrefs_mathml.h"
+ #include "mozilla/intl/UnicodeScriptCodes.h"
++#include "mozilla/OriginAttributes.h"
+ #include "nsDeviceContext.h"
+ #include "nsFontMetrics.h"
+ #include "nsStyleConsts.h"
+@@ -637,6 +638,17 @@ void MathMLTextRunFactory::RebuildTextRun(
+ newFontGroup = fontGroup;
+ }
+
++ // Extract user context ID from PresContext document if available
++ uint32_t userContextId = 0;
++ if (length && styles[0]->mPresContext) {
++ if (mozilla::dom::Document* doc = styles[0]->mPresContext->Document()) {
++ if (nsIPrincipal* principal = doc->NodePrincipal()) {
++ const mozilla::OriginAttributes& attrs = principal->OriginAttributesRef();
++ userContextId = attrs.mUserContextId;
++ }
++ }
++ }
++
+ if (mInnerTransformingTextRunFactory) {
+ transformedChild = mInnerTransformingTextRunFactory->MakeTextRun(
+ convertedString.BeginReading(), convertedString.Length(), &innerParams,
+@@ -646,7 +658,7 @@ void MathMLTextRunFactory::RebuildTextRun(
+ } else {
+ cachedChild = newFontGroup->MakeTextRun(
+ convertedString.BeginReading(), convertedString.Length(), &innerParams,
+- flags, nsTextFrameUtils::Flags(), aMFR);
++ flags, nsTextFrameUtils::Flags(), aMFR, userContextId);
+ child = cachedChild.get();
+ }
+ if (!child) {
+diff --git a/layout/generic/nsTextFrame.cpp b/layout/generic/nsTextFrame.cpp
+index c84d5cde15..7b0e7f4bdf 100644
+--- a/layout/generic/nsTextFrame.cpp
++++ b/layout/generic/nsTextFrame.cpp
+@@ -2338,10 +2338,11 @@ static already_AddRefed GetHyphenTextRun(nsTextFrame* aTextFrame,
+ return fontGroup->MakeHyphenTextRun(dt, flags, appPerDev);
+ }
+ auto* missingFonts = aTextFrame->PresContext()->MissingFontRecorder();
++ uint32_t userContextId = nsLayoutUtils::GetUserContextId(aTextFrame);
+ const NS_ConvertUTF8toUTF16 hyphenStr(hyphenateChar.AsString().AsString());
+ return fontGroup->MakeTextRun(hyphenStr.BeginReading(), hyphenStr.Length(),
+ dt, appPerDev, flags, nsTextFrameUtils::Flags(),
+- missingFonts);
++ missingFonts, userContextId);
+ }
+
+ already_AddRefed BuildTextRunsScanner::BuildTextRunForFrames(
+@@ -2691,6 +2692,9 @@ already_AddRefed BuildTextRunsScanner::BuildTextRunForFrames(
+ "We didn't cover all the characters in the text run!");
+ }
+
++ // Get user context ID for font spacing seed
++ uint32_t userContextId = nsLayoutUtils::GetUserContextId(firstFrame);
++
+ RefPtr textRun;
+ gfxTextRunFactory::Parameters params = {
+ mDrawTarget,
+@@ -2708,7 +2712,7 @@ already_AddRefed BuildTextRunsScanner::BuildTextRunForFrames(
+ std::move(styles), true);
+ } else {
+ textRun = fontGroup->MakeTextRun(text, transformedLength, ¶ms, flags,
+- flags2, mMissingFonts);
++ flags2, mMissingFonts, userContextId);
+ }
+ } else {
+ const uint8_t* text = static_cast(textPtr);
+@@ -2719,7 +2723,7 @@ already_AddRefed BuildTextRunsScanner::BuildTextRunForFrames(
+ std::move(styles), true);
+ } else {
+ textRun = fontGroup->MakeTextRun(text, transformedLength, ¶ms, flags,
+- flags2, mMissingFonts);
++ flags2, mMissingFonts, userContextId);
+ }
+ }
+ if (!textRun) {
+@@ -5561,6 +5565,7 @@ static already_AddRefed GenerateTextRunForEmphasisMarks(
+
+ RefPtr dt = CreateReferenceDrawTarget(aFrame);
+ auto appUnitsPerDevUnit = aFrame->PresContext()->AppUnitsPerDevPixel();
++ uint32_t userContextId = nsLayoutUtils::GetUserContextId(aFrame);
+ gfx::ShapedTextFlags flags =
+ nsLayoutUtils::GetTextRunOrientFlagsForStyle(aComputedStyle);
+ if (flags == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_MIXED) {
+@@ -5569,7 +5574,8 @@ static already_AddRefed GenerateTextRunForEmphasisMarks(
+ }
+ return aFontGroup->MakeTextRun(string.get(), string.Length(), dt,
+ appUnitsPerDevUnit, flags,
+- nsTextFrameUtils::Flags(), nullptr);
++ nsTextFrameUtils::Flags(), nullptr,
++ userContextId);
+ }
+
+ static nsRubyFrame* FindFurthestInlineRubyAncestor(nsTextFrame* aFrame) {
+diff --git a/layout/generic/nsTextRunTransformations.cpp b/layout/generic/nsTextRunTransformations.cpp
+index 93419af8bb..b524540ec6 100644
+--- a/layout/generic/nsTextRunTransformations.cpp
++++ b/layout/generic/nsTextRunTransformations.cpp
+@@ -918,6 +918,10 @@ void nsCaseTransformTextRunFactory::RebuildTextRun(
+ RefPtr cachedChild;
+ gfxTextRun* child;
+
++ // Text transformation contexts don't have direct access to document/frame context,
++ // so we cannot extract private browsing ID. Use 0 (default context).
++ uint32_t privateBrowsingId = 0;
++
+ if (mInnerTransformingTextRunFactory) {
+ transformedChild = mInnerTransformingTextRunFactory->MakeTextRun(
+ convertedString.BeginReading(), convertedString.Length(), &innerParams,
+@@ -927,7 +931,7 @@ void nsCaseTransformTextRunFactory::RebuildTextRun(
+ } else {
+ cachedChild = fontGroup->MakeTextRun(
+ convertedString.BeginReading(), convertedString.Length(), &innerParams,
+- flags, nsTextFrameUtils::Flags(), aMFR);
++ flags, nsTextFrameUtils::Flags(), aMFR, privateBrowsingId);
+ child = cachedChild.get();
+ }
+ if (!child) {
+diff --git a/layout/mathml/nsMathMLChar.cpp b/layout/mathml/nsMathMLChar.cpp
+index 58f9670e7f..577fd833ce 100644
+--- a/layout/mathml/nsMathMLChar.cpp
++++ b/layout/mathml/nsMathMLChar.cpp
+@@ -20,6 +20,8 @@
+ #include "mozilla/StaticPrefs_mathml.h"
+ #include "mozilla/UniquePtr.h"
+ #include "mozilla/dom/Document.h"
++#include "mozilla/OriginAttributes.h"
++#include "mozilla/BasePrincipal.h"
+ #include "mozilla/gfx/2D.h"
+ #include "mozilla/intl/UnicodeScriptCodes.h"
+ #include "nsCOMPtr.h"
+@@ -360,7 +362,7 @@ already_AddRefed nsPropertiesTable::MakeTextRun(
+ "nsPropertiesTable can only access glyphs by code point");
+ return aFontGroup->MakeTextRun(aGlyph.code, aGlyph.Length(), aDrawTarget,
+ aAppUnitsPerDevPixel, mFlags,
+- nsTextFrameUtils::Flags(), nullptr);
++ nsTextFrameUtils::Flags(), nullptr, 0); // TODO: Extract private browsing ID
+ }
+
+ // An instance of nsOpenTypeTable is associated with one gfxFontEntry that
+@@ -431,7 +433,7 @@ void nsOpenTypeTable::UpdateCache(DrawTarget* aDrawTarget,
+ if (mCharCache != aChar) {
+ RefPtr textRun =
+ aFontGroup->MakeTextRun(&aChar, 1, aDrawTarget, aAppUnitsPerDevPixel,
+- mFlags, nsTextFrameUtils::Flags(), nullptr);
++ mFlags, nsTextFrameUtils::Flags(), nullptr, 0);
+ const gfxTextRun::CompressedGlyph& data = textRun->GetCharacterGlyphs()[0];
+ if (data.IsSimpleGlyph()) {
+ mGlyphID = data.GetSimpleGlyph();
+@@ -1396,10 +1398,20 @@ nsresult nsMathMLChar::StretchInternal(
+ flags |= gfx::ShapedTextFlags::TEXT_IS_RTL;
+ }
+
++ uint32_t userContextId = 0;
++ if (mozilla::dom::Document* doc = presContext->Document()) {
++ if (nsIPrincipal* principal = doc->NodePrincipal()) {
++ auto* bp = mozilla::BasePrincipal::Cast(principal);
++ if (bp) {
++ userContextId = bp->OriginAttributesRef().mUserContextId;
++ }
++ }
++ }
++
+ mGlyphs[0] = fm->GetThebesFontGroup()->MakeTextRun(
+ static_cast(mData.get()), len, aDrawTarget,
+ presContext->AppUnitsPerDevPixel(), flags, nsTextFrameUtils::Flags(),
+- presContext->MissingFontRecorder());
++ presContext->MissingFontRecorder(), userContextId);
+ aDesiredStretchSize = MeasureTextRun(aDrawTarget, mGlyphs[0].get());
+
+ bool maxWidth = (NS_STRETCH_MAXWIDTH & aStretchHint) != 0;
+diff --git a/toolkit/components/resistfingerprinting/FontVisibilityProvider.h b/toolkit/components/resistfingerprinting/FontVisibilityProvider.h
+index 51a754b76e..ea41fde83b 100644
+--- a/toolkit/components/resistfingerprinting/FontVisibilityProvider.h
++++ b/toolkit/components/resistfingerprinting/FontVisibilityProvider.h
+@@ -43,6 +43,7 @@ class FontVisibilityProvider {
+ virtual nsICookieJarSettings* GetCookieJarSettings() const = 0;
+ virtual mozilla::Maybe MaybeInheritFontVisibility() const = 0;
+ virtual void UserFontSetUpdated(gfxUserFontEntry* aUpdatedFont = nullptr) = 0;
++ virtual mozilla::dom::Document* GetDocument() const = 0;
+
+ void ReportBlockedFontFamily(const gfxFontFamily& aFamily) const;
+ void ReportBlockedFontFamily(const mozilla::fontlist::Family& aFamily) const;
+@@ -59,6 +60,7 @@ class FontVisibilityProvider {
+ nsICookieJarSettings* GetCookieJarSettings() const override; \
+ mozilla::Maybe MaybeInheritFontVisibility() const override; \
+ void UserFontSetUpdated(gfxUserFontEntry* aUpdatedFont = nullptr) override; \
++ mozilla::dom::Document* GetDocument() const override; \
+ using FontVisibilityProvider::ReportBlockedFontFamily;
+ };
+
diff --git a/patches/audio-context-spoofing.patch b/patches/audio-context-spoofing.patch
index 7857c6d..f6a06f9 100644
--- a/patches/audio-context-spoofing.patch
+++ b/patches/audio-context-spoofing.patch
@@ -23,13 +23,14 @@ diff --git a/dom/media/moz.build b/dom/media/moz.build
index 21063575d7..9e7c9f3e84 100644
--- a/dom/media/moz.build
+++ b/dom/media/moz.build
-@@ -417,3 +417,5 @@ if CONFIG["CC_TYPE"] in ("clang", "gcc"):
+@@ -418,5 +418,7 @@
CXXFLAGS += ["-Werror=switch"]
FINAL_LIBRARY = "xul"
+# DOM Mask
+LOCAL_INCLUDES += ["/camoucfg"]
-\ No newline at end of file
+
+ SPHINX_TREES["/media"] = "docs"
diff --git a/dom/media/webaudio/AudioContext.cpp b/dom/media/webaudio/AudioContext.cpp
index 66184b683b..daf30882ea 100644
--- a/dom/media/webaudio/AudioContext.cpp
diff --git a/patches/config.patch b/patches/config.patch
index fe1b443..ad004ae 100644
--- a/patches/config.patch
+++ b/patches/config.patch
@@ -1,8 +1,8 @@
diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in
-index bb0f71dd60..3ad3d84b82 100644
+index 6bb2e64906..fbdb0b26ea 100644
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
-@@ -260,6 +260,11 @@
+@@ -250,6 +250,11 @@
#endif
#endif
@@ -15,7 +15,7 @@ index bb0f71dd60..3ad3d84b82 100644
; All the pref files must be part of base to prevent migration bugs
@RESPATH@/browser/@PREF_DIR@/firefox.js
diff --git a/lw/moz.build b/lw/moz.build
-index e69de29bb2..208184547e 100644
+index e69de29bb2..5459e4ac36 100644
--- a/lw/moz.build
+++ b/lw/moz.build
@@ -0,0 +1,14 @@
@@ -34,11 +34,13 @@ index e69de29bb2..208184547e 100644
+ "local-settings.js",
+]
diff --git a/moz.build b/moz.build
-index 9db27fd056..e1ec1dc872 100644
+index 4d0f3d6ba9..0f07f37e92 100644
--- a/moz.build
+++ b/moz.build
-@@ -228,3 +228,5 @@ SPHINX_TREES["jsloader"] = "docs/jsloader"
+@@ -235,3 +235,5 @@ SPHINX_TREES["content-security"] = "docs/content-security"
+ SPHINX_TREES["jsloader"] = "docs/jsloader"
include("build/templates.mozbuild")
+
-+DIRS += ["lw"]
\ No newline at end of file
++DIRS += ["lw"]
+\ No newline at end of file
diff --git a/patches/fingerprint-injection.patch b/patches/fingerprint-injection.patch
index 9569e9c..daf3d8b 100644
--- a/patches/fingerprint-injection.patch
+++ b/patches/fingerprint-injection.patch
@@ -1,28 +1,31 @@
diff --git a/browser/app/moz.build b/browser/app/moz.build
-index 434167c996..e18a16aee0 100644
+index a23bb7812a..58942ccb13 100644
--- a/browser/app/moz.build
+++ b/browser/app/moz.build
-@@ -174,3 +174,6 @@ for icon in ("firefox", "document", "newwindow", "newtab", "pbmode", "document_p
- CONFIG["MOZ_BRANDING_DIRECTORY"],
+@@ -182,6 +182,9 @@ for icon in ("firefox", "document", "newwindow", "newtab", "pbmode", "document_p
icon,
)
+
+
+# DOM Mask
+LOCAL_INCLUDES += ["/camoucfg"]
-\ No newline at end of file
+ if CONFIG["MOZ_ASAN"] or CONFIG["MOZ_DEBUG"]:
+ WINCONSOLE = True
+ else:
diff --git a/dom/base/Element.cpp b/dom/base/Element.cpp
-index 807cb4ec25..3df0e0f70f 100644
+index 101590d8c8..f004930b20 100644
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
-@@ -12,6 +12,7 @@
+@@ -12,6 +12,8 @@
#include "mozilla/dom/Element.h"
- #include "mozilla/dom/ElementInlines.h"
-+#include "MaskConfig.hpp"
++#include "MaskConfig.hpp"
++
#include
- #include
-@@ -984,6 +985,18 @@ nsRect Element::GetClientAreaRect() {
+
+ #include
+@@ -1006,6 +1008,18 @@ nsRect Element::GetClientAreaRect() {
Document* doc = OwnerDoc();
nsPresContext* presContext = doc->GetPresContext();
@@ -41,195 +44,11 @@ index 807cb4ec25..3df0e0f70f 100644
// We can avoid a layout flush if this is the scrolling element of the
// document, we have overlay scrollbars, and we aren't embedded in another
// document
-diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp
-index 8e577bd9fd..3ed748a75f 100644
---- a/dom/base/Navigator.cpp
-+++ b/dom/base/Navigator.cpp
-@@ -8,6 +8,7 @@
- #include "base/basictypes.h"
-
- #include "Navigator.h"
-+#include "MaskConfig.hpp"
- #include "nsIXULAppInfo.h"
- #include "nsPluginArray.h"
- #include "nsMimeTypeArray.h"
-@@ -267,6 +268,8 @@ void Navigator::Invalidate() {
-
- void Navigator::GetUserAgent(nsAString& aUserAgent, CallerType aCallerType,
- ErrorResult& aRv) const {
-+ if (auto value = MaskConfig::GetString("navigator.userAgent"))
-+ return aUserAgent.Assign(NS_ConvertUTF8toUTF16(value.value()));
- nsCOMPtr window;
-
- if (mWindow) {
-@@ -293,6 +296,8 @@ void Navigator::GetUserAgent(nsAString& aUserAgent, CallerType aCallerType,
- }
-
- void Navigator::GetAppCodeName(nsAString& aAppCodeName, ErrorResult& aRv) {
-+ if (auto value = MaskConfig::GetString("navigator.appCodeName"))
-+ return aAppCodeName.Assign(NS_ConvertUTF8toUTF16(value.value()));
- nsresult rv;
-
- nsCOMPtr service(
-@@ -314,6 +319,8 @@ void Navigator::GetAppCodeName(nsAString& aAppCodeName, ErrorResult& aRv) {
-
- void Navigator::GetAppVersion(nsAString& aAppVersion, CallerType aCallerType,
- ErrorResult& aRv) const {
-+ if (auto value = MaskConfig::GetString("navigator.appVersion"))
-+ return aAppVersion.Assign(NS_ConvertUTF8toUTF16(value.value()));
- nsCOMPtr doc = mWindow->GetExtantDoc();
-
- nsresult rv = GetAppVersion(
-@@ -325,6 +332,8 @@ void Navigator::GetAppVersion(nsAString& aAppVersion, CallerType aCallerType,
- }
-
- void Navigator::GetAppName(nsAString& aAppName) const {
-+ if (auto value = MaskConfig::GetString("navigator.appName"))
-+ return aAppName.Assign(NS_ConvertUTF8toUTF16(value.value()));
- aAppName.AssignLiteral("Netscape");
- }
-
-@@ -350,6 +359,15 @@ void Navigator::GetAcceptLanguages(const nsString* aLanguageOverride,
-
- aLanguages.Clear();
-
-+ if (std::vector maskValues =
-+ MaskConfig::GetStringList("navigator.languages");
-+ !maskValues.empty()) {
-+ for (const auto& lang : maskValues) {
-+ aLanguages.AppendElement(NS_ConvertUTF8toUTF16(lang));
-+ }
-+ return;
-+ }
-+
- // E.g. "de-de, en-us,en".
- nsAutoString acceptLang;
- if (aLanguageOverride && aLanguageOverride->Length())
-@@ -400,6 +418,8 @@ void Navigator::GetAcceptLanguages(const nsString* aLanguageOverride,
- * Full details above in GetAcceptLanguages.
- */
- void Navigator::GetLanguage(nsAString& aLanguage) {
-+ if (auto value = MaskConfig::GetString("navigator.language"))
-+ return aLanguage.Assign(NS_ConvertUTF8toUTF16(value.value()));
- nsTArray languages;
- GetLanguages(languages);
- MOZ_ASSERT(languages.Length() >= 1);
-@@ -423,6 +443,8 @@ void Navigator::GetLanguages(nsTArray& aLanguages) {
-
- void Navigator::GetPlatform(nsAString& aPlatform, CallerType aCallerType,
- ErrorResult& aRv) const {
-+ if (auto value = MaskConfig::GetString("navigator.platform"))
-+ return aPlatform.Assign(NS_ConvertUTF8toUTF16(value.value()));
- if (mWindow) {
- BrowsingContext* bc = mWindow->GetBrowsingContext();
- nsString customPlatform;
-@@ -449,6 +471,8 @@ void Navigator::GetPlatform(nsAString& aPlatform, CallerType aCallerType,
- void Navigator::GetOscpu(nsAString& aOSCPU, CallerType aCallerType,
- ErrorResult& aRv) const {
- if (aCallerType != CallerType::System) {
-+ if (auto value = MaskConfig::GetString("navigator.oscpu"))
-+ return aOSCPU.Assign(NS_ConvertUTF8toUTF16(value.value()));
- // If fingerprinting resistance is on, we will spoof this value. See
- // nsRFPService.h for details about spoofed values.
- if (nsContentUtils::ShouldResistFingerprinting(GetDocShell(),
-@@ -488,10 +512,14 @@ void Navigator::GetVendor(nsAString& aVendor) { aVendor.Truncate(); }
- void Navigator::GetVendorSub(nsAString& aVendorSub) { aVendorSub.Truncate(); }
-
- void Navigator::GetProduct(nsAString& aProduct) {
-+ if (auto value = MaskConfig::GetString("navigator.product"))
-+ return aProduct.Assign(NS_ConvertUTF8toUTF16(value.value()));
- aProduct.AssignLiteral("Gecko");
- }
-
- void Navigator::GetProductSub(nsAString& aProductSub) {
-+ if (auto value = MaskConfig::GetString("navigator.productSub"))
-+ return aProductSub.Assign(NS_ConvertUTF8toUTF16(value.value()));
- // Legacy build date hardcoded for backward compatibility (bug 776376)
- aProductSub.AssignLiteral(LEGACY_UA_GECKO_TRAIL);
- }
-@@ -517,7 +545,11 @@ nsPluginArray* Navigator::GetPlugins(ErrorResult& aRv) {
- return mPlugins;
- }
-
--bool Navigator::PdfViewerEnabled() { return !StaticPrefs::pdfjs_disabled(); }
-+bool Navigator::PdfViewerEnabled() {
-+ if (auto value = MaskConfig::GetBool("pdfViewerEnabled"); value.has_value())
-+ return value.value();
-+ return !StaticPrefs::pdfjs_disabled();
-+}
-
- Permissions* Navigator::GetPermissions(ErrorResult& aRv) {
- if (!mWindow) {
-@@ -543,6 +575,9 @@ StorageManager* Navigator::Storage() {
- }
-
- bool Navigator::CookieEnabled() {
-+ if (auto value = MaskConfig::GetBool("navigator.cookieEnabled");
-+ value.has_value())
-+ return value.value();
- // Check whether an exception overrides the global cookie behavior
- // Note that the code for getting the URI here matches that in
- // nsHTMLDocument::SetCookie.
-@@ -589,6 +624,8 @@ bool Navigator::CookieEnabled() {
- }
-
- bool Navigator::OnLine() {
-+ if (auto value = MaskConfig::GetBool("navigator.onLine"); value.has_value())
-+ return value.value();
- if (mWindow) {
- // Check if this tab is set to be offline.
- BrowsingContext* bc = mWindow->GetBrowsingContext();
-@@ -603,6 +640,8 @@ bool Navigator::OnLine() {
- void Navigator::GetBuildID(nsAString& aBuildID, CallerType aCallerType,
- ErrorResult& aRv) const {
- if (aCallerType != CallerType::System) {
-+ if (auto value = MaskConfig::GetString("navigator.buildID"))
-+ return aBuildID.Assign(NS_ConvertUTF8toUTF16(value.value()));
- // If fingerprinting resistance is on, we will spoof this value. See
- // nsRFPService.h for details about spoofed values.
- if (nsContentUtils::ShouldResistFingerprinting(
-@@ -659,6 +698,8 @@ void Navigator::GetBuildID(nsAString& aBuildID, CallerType aCallerType,
- }
-
- void Navigator::GetDoNotTrack(nsAString& aResult) {
-+ if (auto value = MaskConfig::GetString("navigator.doNotTrack"))
-+ return aResult.Assign(NS_ConvertUTF8toUTF16(value.value()));
- bool doNotTrack = StaticPrefs::privacy_donottrackheader_enabled();
- if (!doNotTrack) {
- nsCOMPtr loadContext = do_GetInterface(mWindow);
-@@ -673,6 +714,9 @@ void Navigator::GetDoNotTrack(nsAString& aResult) {
- }
-
- bool Navigator::GlobalPrivacyControl() {
-+ if (auto value = MaskConfig::GetBool("navigator.globalPrivacyControl");
-+ value.has_value())
-+ return value.value();
- bool gpcStatus = StaticPrefs::privacy_globalprivacycontrol_enabled();
- if (!gpcStatus) {
- nsCOMPtr loadContext = do_GetInterface(mWindow);
-@@ -684,6 +728,8 @@ bool Navigator::GlobalPrivacyControl() {
- }
-
- uint64_t Navigator::HardwareConcurrency() {
-+ if (auto value = MaskConfig::GetUint64("navigator.hardwareConcurrency"))
-+ return value.value();
- workerinternals::RuntimeService* rts =
- workerinternals::RuntimeService::GetOrCreateService();
- if (!rts) {
-@@ -882,6 +928,8 @@ bool Navigator::Vibrate(const nsTArray& aPattern) {
- //*****************************************************************************
-
- uint32_t Navigator::MaxTouchPoints(CallerType aCallerType) {
-+ if (auto value = MaskConfig::GetUint32("navigator.maxTouchPoints"))
-+ return value.value();
- nsIDocShell* docshell = GetDocShell();
- BrowsingContext* bc = docshell ? docshell->GetBrowsingContext() : nullptr;
-
diff --git a/dom/base/moz.build b/dom/base/moz.build
-index 114402c4a1..55daeabe7a 100644
+index cd9090cda3..37a053579e 100644
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
-@@ -641,3 +641,6 @@ GeneratedFile(
+@@ -634,3 +634,6 @@ GeneratedFile(
"/servo/components/style/properties/counted_unknown_properties.py",
],
)
@@ -238,7 +57,7 @@ index 114402c4a1..55daeabe7a 100644
+LOCAL_INCLUDES += ["/camoucfg"]
\ No newline at end of file
diff --git a/dom/base/nsGlobalWindowInner.cpp b/dom/base/nsGlobalWindowInner.cpp
-index 3678ebc59c..03a72fe242 100644
+index 330a707789..241f87780b 100644
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -5,6 +5,7 @@
@@ -249,7 +68,7 @@ index 3678ebc59c..03a72fe242 100644
#include
#include
-@@ -3410,6 +3411,8 @@ void nsGlobalWindowInner::SetName(const nsAString& aName,
+@@ -3439,6 +3440,8 @@ void nsGlobalWindowInner::SetName(const nsAString& aName,
}
double nsGlobalWindowInner::GetInnerWidth(ErrorResult& aError) {
@@ -258,7 +77,7 @@ index 3678ebc59c..03a72fe242 100644
FORWARD_TO_OUTER_OR_THROW(GetInnerWidthOuter, (aError), aError, 0);
}
-@@ -3421,6 +3424,8 @@ nsresult nsGlobalWindowInner::GetInnerWidth(double* aWidth) {
+@@ -3450,6 +3453,8 @@ nsresult nsGlobalWindowInner::GetInnerWidth(double* aWidth) {
}
double nsGlobalWindowInner::GetInnerHeight(ErrorResult& aError) {
@@ -267,7 +86,7 @@ index 3678ebc59c..03a72fe242 100644
// We ignore aCallerType; we only have that argument because some other things
// called by GetReplaceableWindowCoord need it. If this ever changes, fix
// nsresult nsGlobalWindowInner::GetInnerHeight(double* aInnerWidth)
-@@ -3437,12 +3442,16 @@ nsresult nsGlobalWindowInner::GetInnerHeight(double* aHeight) {
+@@ -3466,12 +3471,16 @@ nsresult nsGlobalWindowInner::GetInnerHeight(double* aHeight) {
int32_t nsGlobalWindowInner::GetOuterWidth(CallerType aCallerType,
ErrorResult& aError) {
@@ -284,7 +103,7 @@ index 3678ebc59c..03a72fe242 100644
FORWARD_TO_OUTER_OR_THROW(GetOuterHeightOuter, (aCallerType, aError), aError,
0);
}
-@@ -3457,11 +3466,13 @@ double nsGlobalWindowInner::ScreenEdgeSlopY() const {
+@@ -3486,11 +3495,13 @@ double nsGlobalWindowInner::ScreenEdgeSlopY() const {
int32_t nsGlobalWindowInner::GetScreenX(CallerType aCallerType,
ErrorResult& aError) {
@@ -298,7 +117,7 @@ index 3678ebc59c..03a72fe242 100644
FORWARD_TO_OUTER_OR_THROW(GetScreenYOuter, (aCallerType, aError), aError, 0);
}
-@@ -3495,6 +3506,8 @@ static nsPresContext* GetPresContextForRatio(Document* aDoc) {
+@@ -3524,6 +3535,8 @@ static nsPresContext* GetPresContextForRatio(Document* aDoc) {
double nsGlobalWindowInner::GetDevicePixelRatio(CallerType aCallerType,
ErrorResult& aError) {
ENSURE_ACTIVE_DOCUMENT(aError, 0.0);
@@ -307,7 +126,7 @@ index 3678ebc59c..03a72fe242 100644
RefPtr presContext = GetPresContextForRatio(mDoc);
if (NS_WARN_IF(!presContext)) {
-@@ -3565,26 +3578,38 @@ already_AddRefed nsGlobalWindowInner::MatchMedia(
+@@ -3594,26 +3607,38 @@ already_AddRefed nsGlobalWindowInner::MatchMedia(
}
int32_t nsGlobalWindowInner::GetScrollMinX(ErrorResult& aError) {
@@ -347,7 +166,7 @@ index 3678ebc59c..03a72fe242 100644
}
diff --git a/dom/base/nsHistory.cpp b/dom/base/nsHistory.cpp
-index 99994a73cc..837214d872 100644
+index e2fd8e6389..e5ddf4e08c 100644
--- a/dom/base/nsHistory.cpp
+++ b/dom/base/nsHistory.cpp
@@ -5,6 +5,7 @@
@@ -357,8 +176,8 @@ index 99994a73cc..837214d872 100644
+#include "MaskConfig.hpp"
#include "jsapi.h"
- #include "nsCOMPtr.h"
-@@ -55,6 +56,8 @@ JSObject* nsHistory::WrapObject(JSContext* aCx,
+ #include "mozilla/RefPtr.h"
+@@ -64,6 +65,8 @@ JSObject* nsHistory::WrapObject(JSContext* aCx,
}
uint32_t nsHistory::GetLength(ErrorResult& aRv) const {
@@ -368,18 +187,19 @@ index 99994a73cc..837214d872 100644
if (!win || !win->HasActiveDocument()) {
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
diff --git a/dom/base/nsScreen.cpp b/dom/base/nsScreen.cpp
-index 4bd1e0c964..a5a95d3fdf 100644
+index 306ab35772..7f7728a6cc 100644
--- a/dom/base/nsScreen.cpp
+++ b/dom/base/nsScreen.cpp
-@@ -4,6 +4,7 @@
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+@@ -6,6 +6,8 @@
+
+ #include "nsScreen.h"
+#include "MaskConfig.hpp"
- #include "nsContentUtils.h"
- #include "nsScreen.h"
++
+ #include "mozilla/GeckoBindings.h"
#include "mozilla/dom/Document.h"
-@@ -37,6 +38,10 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED(nsScreen, DOMEventTargetHelper,
+ #include "mozilla/dom/DocumentInlines.h"
+@@ -40,6 +42,10 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED(nsScreen, DOMEventTargetHelper,
mScreenOrientation)
int32_t nsScreen::PixelDepth() {
@@ -390,7 +210,7 @@ index 4bd1e0c964..a5a95d3fdf 100644
// Return 24 to prevent fingerprinting.
if (ShouldResistFingerprinting(RFPTarget::ScreenPixelDepth)) {
return 24;
-@@ -89,6 +94,12 @@ CSSIntRect nsScreen::GetRect() {
+@@ -89,6 +95,12 @@ CSSIntRect nsScreen::GetRect() {
}
CSSIntRect nsScreen::GetAvailRect() {
@@ -402,20 +222,20 @@ index 4bd1e0c964..a5a95d3fdf 100644
+ }
// Return window inner rect to prevent fingerprinting.
if (ShouldResistFingerprinting(RFPTarget::ScreenAvailRect)) {
- return GetWindowInnerRect();
+ return GetTopWindowInnerRectForRFP();
diff --git a/dom/battery/BatteryManager.cpp b/dom/battery/BatteryManager.cpp
-index a42ac3017d..fadae4a714 100644
+index 6322093fd9..264cc91bd1 100644
--- a/dom/battery/BatteryManager.cpp
+++ b/dom/battery/BatteryManager.cpp
-@@ -7,6 +7,7 @@
+@@ -9,6 +9,7 @@
#include
#include
- #include "BatteryManager.h"
+
+#include "MaskConfig.hpp"
#include "Constants.h"
#include "mozilla/DOMEventTargetHelper.h"
#include "mozilla/Hal.h"
-@@ -51,6 +52,9 @@ JSObject* BatteryManager::WrapObject(JSContext* aCx,
+@@ -53,6 +54,9 @@ JSObject* BatteryManager::WrapObject(JSContext* aCx,
bool BatteryManager::Charging() const {
MOZ_ASSERT(NS_IsMainThread());
@@ -425,7 +245,7 @@ index a42ac3017d..fadae4a714 100644
// For testing, unable to report the battery status information
if (Preferences::GetBool("dom.battery.test.default", false)) {
return true;
-@@ -67,6 +71,8 @@ bool BatteryManager::Charging() const {
+@@ -69,6 +73,8 @@ bool BatteryManager::Charging() const {
double BatteryManager::DischargingTime() const {
MOZ_ASSERT(NS_IsMainThread());
@@ -434,7 +254,7 @@ index a42ac3017d..fadae4a714 100644
// For testing, unable to report the battery status information
if (Preferences::GetBool("dom.battery.test.default", false)) {
return std::numeric_limits::infinity();
-@@ -84,6 +90,8 @@ double BatteryManager::DischargingTime() const {
+@@ -86,6 +92,8 @@ double BatteryManager::DischargingTime() const {
double BatteryManager::ChargingTime() const {
MOZ_ASSERT(NS_IsMainThread());
@@ -443,7 +263,7 @@ index a42ac3017d..fadae4a714 100644
// For testing, unable to report the battery status information
if (Preferences::GetBool("dom.battery.test.default", false)) {
return 0.0;
-@@ -101,6 +109,7 @@ double BatteryManager::ChargingTime() const {
+@@ -103,6 +111,7 @@ double BatteryManager::ChargingTime() const {
double BatteryManager::Level() const {
MOZ_ASSERT(NS_IsMainThread());
@@ -464,7 +284,7 @@ index 3a90c93c01..91d673039b 100644
+LOCAL_INCLUDES += ["/camoucfg"]
\ No newline at end of file
diff --git a/dom/workers/WorkerNavigator.cpp b/dom/workers/WorkerNavigator.cpp
-index b05e409a84..8e0f7ea657 100644
+index 622724b529..c9a65ff013 100644
--- a/dom/workers/WorkerNavigator.cpp
+++ b/dom/workers/WorkerNavigator.cpp
@@ -5,6 +5,7 @@
@@ -475,7 +295,7 @@ index b05e409a84..8e0f7ea657 100644
#include
-@@ -92,6 +93,9 @@ JSObject* WorkerNavigator::WrapObject(JSContext* aCx,
+@@ -103,6 +104,9 @@ JSObject* WorkerNavigator::WrapObject(JSContext* aCx,
}
bool WorkerNavigator::GlobalPrivacyControl() const {
@@ -485,7 +305,7 @@ index b05e409a84..8e0f7ea657 100644
bool gpcStatus = StaticPrefs::privacy_globalprivacycontrol_enabled();
if (!gpcStatus) {
JSObject* jso = GetWrapper();
-@@ -114,6 +118,8 @@ void WorkerNavigator::SetLanguages(const nsTArray& aLanguages) {
+@@ -125,6 +129,8 @@ void WorkerNavigator::SetLanguages(const nsTArray& aLanguages) {
void WorkerNavigator::GetAppVersion(nsString& aAppVersion,
CallerType aCallerType,
ErrorResult& aRv) const {
@@ -494,7 +314,7 @@ index b05e409a84..8e0f7ea657 100644
WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
MOZ_ASSERT(workerPrivate);
-@@ -136,6 +142,8 @@ void WorkerNavigator::GetAppVersion(nsString& aAppVersion,
+@@ -147,6 +153,8 @@ void WorkerNavigator::GetAppVersion(nsString& aAppVersion,
void WorkerNavigator::GetPlatform(nsString& aPlatform, CallerType aCallerType,
ErrorResult& aRv) const {
@@ -503,7 +323,7 @@ index b05e409a84..8e0f7ea657 100644
WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
MOZ_ASSERT(workerPrivate);
-@@ -200,6 +208,8 @@ class GetUserAgentRunnable final : public WorkerMainThreadRunnable {
+@@ -207,6 +215,8 @@ class GetUserAgentRunnable final : public WorkerMainThreadRunnable {
void WorkerNavigator::GetUserAgent(nsString& aUserAgent, CallerType aCallerType,
ErrorResult& aRv) const {
@@ -512,7 +332,7 @@ index b05e409a84..8e0f7ea657 100644
WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
MOZ_ASSERT(workerPrivate);
-@@ -211,6 +221,8 @@ void WorkerNavigator::GetUserAgent(nsString& aUserAgent, CallerType aCallerType,
+@@ -218,6 +228,8 @@ void WorkerNavigator::GetUserAgent(nsString& aUserAgent, CallerType aCallerType,
}
uint64_t WorkerNavigator::HardwareConcurrency() const {
@@ -522,10 +342,10 @@ index b05e409a84..8e0f7ea657 100644
MOZ_ASSERT(rts);
diff --git a/dom/workers/moz.build b/dom/workers/moz.build
-index c7818826d1..2e2e6526e7 100644
+index 2f2948a729..b8ad10403f 100644
--- a/dom/workers/moz.build
+++ b/dom/workers/moz.build
-@@ -112,3 +112,6 @@ MARIONETTE_MANIFESTS += ["test/marionette/manifest.toml"]
+@@ -114,3 +114,6 @@ MARIONETTE_MANIFESTS += ["test/marionette/manifest.toml"]
XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"]
BROWSER_CHROME_MANIFESTS += ["test/browser.toml"]
diff --git a/patches/font-hijacker.patch b/patches/font-hijacker.patch
index 08fd76c..85d06bc 100644
--- a/patches/font-hijacker.patch
+++ b/patches/font-hijacker.patch
@@ -31,9 +31,9 @@ diff --git a/gfx/thebes/moz.build b/gfx/thebes/moz.build
index 3d99906827..a9d1191688 100644
--- a/gfx/thebes/moz.build
+++ b/gfx/thebes/moz.build
-@@ -305,3 +305,6 @@ DEFINES["GRAPHITE2_STATIC"] = True
+@@ -301,3 +301,6 @@ DEFINES["GRAPHITE2_STATIC"] = True
CXXFLAGS += ["-Werror=switch"]
-
+
include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+# DOM Mask
@@ -84,7 +84,7 @@ diff --git a/layout/style/FontFaceImpl.cpp b/layout/style/FontFaceImpl.cpp
index 9d1f7c8c5b..926315ca2f 100644
--- a/layout/style/FontFaceImpl.cpp
+++ b/layout/style/FontFaceImpl.cpp
-@@ -357,21 +357,15 @@ void FontFaceImpl::DoLoad() {
+@@ -357,22 +357,16 @@ void FontFaceImpl::DoLoad() {
void FontFaceImpl::SetStatus(FontFaceLoadStatus aStatus) {
gfxFontUtils::AssertSafeThreadOrServoFontMetricsLocked();
@@ -112,19 +112,20 @@ index 9d1f7c8c5b..926315ca2f 100644
if (mInFontFaceSet) {
mFontFaceSet->OnFontFaceStatusChanged(this);
}
+
diff --git a/layout/style/FontFaceImpl.h b/layout/style/FontFaceImpl.h
index 70c06609e9..66b2666621 100644
--- a/layout/style/FontFaceImpl.h
+++ b/layout/style/FontFaceImpl.h
-@@ -8,6 +8,7 @@
- #define mozilla_dom_FontFaceImpl_h
-
+@@ -13,6 +13,7 @@
+ #include "mozilla/ServoStyleConsts.h"
#include "mozilla/dom/FontFaceBinding.h"
+#include "MaskConfig.hpp"
- #include "mozilla/FontPropertyTypes.h"
- #include "mozilla/Maybe.h"
- #include "mozilla/Mutex.h"
-@@ -35,6 +36,20 @@ class UTF8StringOrArrayBufferOrArrayBufferView;
+ #include "nsTHashSet.h"
+
+ class gfxFontFaceBufferSource;
+
+@@ -33,6 +34,20 @@
namespace mozilla::dom {
@@ -149,11 +150,11 @@ diff --git a/layout/style/moz.build b/layout/style/moz.build
index a14ab6a7ac..6e6a727abf 100644
--- a/layout/style/moz.build
+++ b/layout/style/moz.build
-@@ -351,3 +351,6 @@ if CONFIG["COMPILE_ENVIRONMENT"]:
- CONFIGURE_SUBST_FILES += [
- "extra-bindgen-flags",
- ]
+@@ -378,4 +378,7 @@
+ CbindgenHeader(
+ "ServoStyleConsts.h",
+ inputs=["/servo/ports/geckolib", "/servo/components/style"],
+ )
+
+# DOM Mask
+LOCAL_INCLUDES += ["/camoucfg"]
-\ No newline at end of file
diff --git a/patches/geolocation-spoofing.patch b/patches/geolocation-spoofing.patch
index 9606c64..8abf921 100644
--- a/patches/geolocation-spoofing.patch
+++ b/patches/geolocation-spoofing.patch
@@ -73,19 +73,20 @@ diff --git a/dom/geolocation/moz.build b/dom/geolocation/moz.build
index 2d6b6b5fab..0c7ed74c6d 100644
--- a/dom/geolocation/moz.build
+++ b/dom/geolocation/moz.build
-@@ -58,3 +58,6 @@ elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
- if CONFIG["MOZ_ENABLE_DBUS"]:
- LOCAL_INCLUDES += ["/dom/system/linux"]
- CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+@@ -85,4 +85,7 @@
+ BROWSER_CHROME_MANIFESTS += ["test/browser/browser.toml"]
+ MOCHITEST_CHROME_MANIFESTS += ["test/chrome/chrome.toml"]
+ MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.toml"]
+ XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.toml"]
+
+# DOM Mask
+LOCAL_INCLUDES += ["/camoucfg"]
-\ No newline at end of file
+
diff --git a/dom/system/NetworkGeolocationProvider.sys.mjs b/dom/system/NetworkGeolocationProvider.sys.mjs
index 7edc62b4a6..19b77aa6b6 100644
--- a/dom/system/NetworkGeolocationProvider.sys.mjs
+++ b/dom/system/NetworkGeolocationProvider.sys.mjs
-@@ -442,17 +442,55 @@ NetworkGeolocationProvider.prototype = {
+@@ -342,17 +342,55 @@ NetworkGeolocationProvider.prototype = {
let url = Services.urlFormatter.formatURLPref("geo.provider.network.url");
LOG("Sending request");
diff --git a/patches/ghostery/Disable-Onboarding-Messages.patch b/patches/ghostery/Disable-Onboarding-Messages.patch
index 2e22aea..2ccb7cb 100644
--- a/patches/ghostery/Disable-Onboarding-Messages.patch
+++ b/patches/ghostery/Disable-Onboarding-Messages.patch
@@ -12,15 +12,11 @@ index ceded6b755..90aa3abe36 100644
+++ b/browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs
@@ -1213,8 +1213,7 @@ const BASE_MESSAGES = () => [
];
-
+
// Eventually, move Feature Callout messages to their own provider
-const ONBOARDING_MESSAGES = () =>
- BASE_MESSAGES().concat(FeatureCalloutMessages.getMessages());
+const ONBOARDING_MESSAGES = () => ([]);
-
+
export const OnboardingMessageProvider = {
async getExtraAttributes() {
-
---
-2.39.2 (Apple Git-143)
-
diff --git a/patches/ghostery/Disable-Onboarding-Messages.patch.bak b/patches/ghostery/Disable-Onboarding-Messages.patch.bak
new file mode 100644
index 0000000..f9ffefb
--- /dev/null
+++ b/patches/ghostery/Disable-Onboarding-Messages.patch.bak
@@ -0,0 +1,23 @@
+From: Jenkins
+Date: Fri, 3 Mar 2023 09:03:01 +0100
+Subject: Disable Onboarding Messages
+
+---
+ browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs | 3 +--
+ 1 file changed, 1 insertion(+), 2 deletions(-)
+
+diff --git a/browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs b/browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs
+index ceded6b755..90aa3abe36 100644
+--- a/browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs
++++ b/browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs
+@@ -1213,11 +1213,9 @@ const BASE_MESSAGES = () => [
+ ];
+
+ // Eventually, move Feature Callout messages to their own provider
+-const ONBOARDING_MESSAGES = () =>
+- BASE_MESSAGES().concat(FeatureCalloutMessages.getMessages());
++const ONBOARDING_MESSAGES = () => ([]);
+
+ export const OnboardingMessageProvider = {
+ async getExtraAttributes() {
+
diff --git a/patches/librewolf/context-menu.patch b/patches/librewolf/context-menu.patch
deleted file mode 100644
index b0e9fa4..0000000
--- a/patches/librewolf/context-menu.patch
+++ /dev/null
@@ -1,24 +0,0 @@
-diff --git a/browser/base/content/browser-context.inc b/browser/base/content/browser-context.inc
-index 3a58888b28b4..471c238aa284 100644
---- a/browser/base/content/browser-context.inc
-+++ b/browser/base/content/browser-context.inc
-@@ -107,9 +107,6 @@
-
--
-
-@@ -264,9 +261,6 @@
-
--
-