fix: migrate Juggler modules from JSM to ESM for Firefox 146 compatibility

- Converted ChannelEventSink from JSM to ESM (.sys.mjs)
- Changed ChromeUtils.import() to ChromeUtils.importESModule() across all modules
- Updated .jsm file extensions to .sys.mjs for system module imports
- Replaced EXPORTED_SYMBOLS with export keyword in Helper.js, NetworkObserver.js, SimpleChannel.js, TargetRegistry.js, and JugglerFrameParent.jsm
- Updated EventEmitter import from resource://gre/modules/EventEmitter.jsm to .
This commit is contained in:
Nirupam Bhowmick
2025-12-30 05:13:18 +00:00
parent 387494bef5
commit 03c12302b0
22 changed files with 544 additions and 431 deletions
Vendored
BIN
View File
Binary file not shown.
@@ -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;
};
+3 -7
View File
@@ -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;
+4 -6
View File
@@ -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;
+68 -32
View File
@@ -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;
+1 -4
View File
@@ -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;
+80 -56
View File
@@ -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;
+1
View File
@@ -158,3 +158,4 @@ const jugglerInstance = new Juggler();
export var JugglerFactory = function() {
return jugglerInstance;
};
+1 -1
View File
@@ -15,4 +15,4 @@ Classes = [
"constructor": "JugglerFactory",
},
]
+16 -49
View File
@@ -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;
@@ -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'];
+6 -9
View File
@@ -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;
+5 -100
View File
@@ -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;
+25 -20
View File
@@ -2,20 +2,26 @@
* 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');
// 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 +31,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 +99,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 +122,3 @@ function initialize(browsingContext, docShell) {
return data;
}
var EXPORTED_SYMBOLS = ['initialize'];
this.initialize = initialize;
+1
View File
@@ -9,6 +9,7 @@ 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)
+15 -11
View File
@@ -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;
+4 -62
View File
@@ -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}]`;
}
}
+27 -59
View File
@@ -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;
+2 -5
View File
@@ -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 = ['<root>']) {
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 = ['<root>']) {
export function checkScheme(scheme, x, details = {}, path = ['<root>']) {
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'];
+8 -4
View File
@@ -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'];
@@ -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;
+165
View File
@@ -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<nsICommandLineHandler>`.
**Common Error:** If you see:
```
ld64.lld: error: undefined symbol: already_AddRefed<nsISupports> mozCreateComponent<nsICommandLineHandler>()
```
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)