feat: Main world JS evaluation

Experimental support to execute in the main world. Usage: `page.evaluate("mw:<script>")`
Has only been implemented to pass JSON serializable objects to/from the main world (Isolated worlds are still the default, and should be used unless necessary).
This commit is contained in:
daijro 2024-12-03 06:36:31 -06:00
parent 3e524aa2ea
commit 4305385f0b
4 changed files with 105 additions and 1 deletions

View file

@ -517,6 +517,11 @@ class Frame {
frameId: this.id(),
name,
});
// Camoufox: Create a main world for the isolated context
if (ChromeUtils.camouGetBool('allowMainWorld', false)) {
const mainWorld = this._runtime.createMW(this.domWindow(), this.domWindow());
world.mainEquivalent = mainWorld;
}
this._worldNameToContext.set(name, world);
return world;
}

View file

@ -99,6 +99,36 @@ 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 || [];
try {
const exceptionDetails = {};
const result = mainContext.executeInGlobal(mainWorldScript, functionArgs, exceptionDetails);
if (!result)
return {exceptionDetails};
return {result};
} catch (e) {
throw e;
}
}
const exceptionDetails = {};
let result = await executionContext.evaluateFunction(functionDeclaration, args, exceptionDetails);
if (!result)
@ -106,7 +136,7 @@ class Runtime {
if (returnByValue)
result = executionContext.ensureSerializedToValue(result);
return {result};
}
}
async getObjectProperties({executionContextId, objectId}) {
const executionContext = this.findExecutionContext(executionContextId);
@ -282,6 +312,11 @@ 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)
@ -306,6 +341,68 @@ 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 = `
(() => {
const result = (${script});
return typeof result === 'function'
? result(${args.map(arg => JSON.stringify(arg)).join(', ')})
: result;
})()
`;
const result = this._debuggee.executeInGlobal(wrappedScript);
let {success, obj} = this._getResult(result, exceptionDetails);
if (!success) {
return {exceptionDetails};
}
return {value: obj};
} catch (e) {
exceptionDetails.text = e.message;
exceptionDetails.stack = e.stack;
return {exceptionDetails};
}
}
}
class ExecutionContext {
constructor(runtime, domWindow, contextGlobal, auxData) {
this._runtime = runtime;

View file

@ -288,6 +288,7 @@
"humanize:minTime": "double[>=0]",
"showcursor": "bool",
"allowMainWorld": "bool",
"memorysaver": "bool",
"addons": "array[str]",
"debug": "bool"

View file

@ -91,6 +91,7 @@
{ "property": "mediaDevices:webcams", "type": "uint" },
{ "property": "mediaDevices:speakers", "type": "uint" },
{ "property": "mediaDevices:enabled", "type": "bool" },
{ "property": "allowMainWorld", "type": "bool" },
{ "property": "memorysaver", "type": "bool" },
{ "property": "addons", "type": "array" },
{ "property": "debug", "type": "bool" }