Skip to content

Commit

Permalink
feat: Main world JS evaluation
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
daijro committed Dec 3, 2024
1 parent 3e524aa commit 4305385
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 1 deletion.
5 changes: 5 additions & 0 deletions additions/juggler/content/FrameTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
99 changes: 98 additions & 1 deletion additions/juggler/content/Runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,44 @@ 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)
return {exceptionDetails};
if (returnByValue)
result = executionContext.ensureSerializedToValue(result);
return {result};
}
}

async getObjectProperties({executionContextId, objectId}) {
const executionContext = this.findExecutionContext(executionContextId);
Expand Down Expand Up @@ -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)
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions settings/camoucfg.jvv
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@
"humanize:minTime": "double[>=0]",
"showcursor": "bool",

"allowMainWorld": "bool",
"memorysaver": "bool",
"addons": "array[str]",
"debug": "bool"
Expand Down
1 change: 1 addition & 0 deletions settings/properties.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down

0 comments on commit 4305385

Please sign in to comment.