439 lines
13 KiB
JavaScript
439 lines
13 KiB
JavaScript
/* 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 { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
|
|
getFramesFromStack: "chrome://remote/content/shared/Stack.sys.mjs",
|
|
isChromeFrame: "chrome://remote/content/shared/Stack.sys.mjs",
|
|
OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
|
|
setDefaultSerializationOptions:
|
|
"chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
|
|
stringify: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
|
|
});
|
|
|
|
/**
|
|
* @typedef {string} EvaluationStatus
|
|
*/
|
|
|
|
/**
|
|
* Enum of possible evaluation states.
|
|
*
|
|
* @readonly
|
|
* @enum {EvaluationStatus}
|
|
*/
|
|
const EvaluationStatus = {
|
|
Normal: "normal",
|
|
Throw: "throw",
|
|
};
|
|
|
|
class ScriptModule extends WindowGlobalBiDiModule {
|
|
destroy() {}
|
|
|
|
#buildExceptionDetails(
|
|
exception,
|
|
stack,
|
|
realm,
|
|
resultOwnership,
|
|
seenNodeIds
|
|
) {
|
|
exception = this.#toRawObject(exception);
|
|
|
|
// A stacktrace is mandatory to build exception details and a missing stack
|
|
// means we encountered an unexpected issue. Throw with an explicit error.
|
|
if (!stack) {
|
|
throw new Error(
|
|
`Missing stack, unable to build exceptionDetails for exception: ${lazy.stringify(
|
|
exception
|
|
)}`
|
|
);
|
|
}
|
|
|
|
const frames = lazy.getFramesFromStack(stack) || [];
|
|
const callFrames = frames
|
|
// Remove chrome/internal frames
|
|
.filter(frame => !lazy.isChromeFrame(frame))
|
|
// Translate frames from getFramesFromStack to frames expected by
|
|
// WebDriver BiDi.
|
|
.map(frame => {
|
|
return {
|
|
columnNumber: frame.columnNumber - 1,
|
|
functionName: frame.functionName,
|
|
lineNumber: frame.lineNumber - 1,
|
|
url: frame.filename,
|
|
};
|
|
});
|
|
|
|
return {
|
|
columnNumber: stack.column - 1,
|
|
exception: this.serialize(
|
|
exception,
|
|
lazy.setDefaultSerializationOptions(),
|
|
resultOwnership,
|
|
realm,
|
|
{ seenNodeIds }
|
|
),
|
|
lineNumber: stack.line - 1,
|
|
stackTrace: { callFrames },
|
|
text: lazy.stringify(exception),
|
|
};
|
|
}
|
|
|
|
async #buildReturnValue(
|
|
rv,
|
|
realm,
|
|
awaitPromise,
|
|
resultOwnership,
|
|
serializationOptions
|
|
) {
|
|
let evaluationStatus, exception, result, stack;
|
|
|
|
if ("return" in rv) {
|
|
evaluationStatus = EvaluationStatus.Normal;
|
|
if (
|
|
awaitPromise &&
|
|
// Only non-primitive return values are wrapped in Debugger.Object.
|
|
rv.return instanceof Debugger.Object &&
|
|
rv.return.isPromise
|
|
) {
|
|
try {
|
|
// Force wrapping the promise resolution result in a Debugger.Object
|
|
// wrapper for consistency with the synchronous codepath.
|
|
const asyncResult = await rv.return.unsafeDereference();
|
|
result = realm.globalObjectReference.makeDebuggeeValue(asyncResult);
|
|
} catch (asyncException) {
|
|
evaluationStatus = EvaluationStatus.Throw;
|
|
exception =
|
|
realm.globalObjectReference.makeDebuggeeValue(asyncException);
|
|
|
|
// If the returned promise was rejected by calling its reject callback
|
|
// the stack will be available on promiseResolutionSite.
|
|
// Otherwise, (eg. rejected Promise chained with a then() call) we
|
|
// fallback on the promiseAllocationSite.
|
|
stack =
|
|
rv.return.promiseResolutionSite || rv.return.promiseAllocationSite;
|
|
}
|
|
} else {
|
|
// rv.return is a Debugger.Object or a primitive.
|
|
result = rv.return;
|
|
}
|
|
} else if ("throw" in rv) {
|
|
// rv.throw will be set if the evaluation synchronously failed, either if
|
|
// the script contains a syntax error or throws an exception.
|
|
evaluationStatus = EvaluationStatus.Throw;
|
|
exception = rv.throw;
|
|
stack = rv.stack;
|
|
}
|
|
|
|
const seenNodeIds = new Map();
|
|
switch (evaluationStatus) {
|
|
case EvaluationStatus.Normal: {
|
|
const dataSuccess = this.serialize(
|
|
this.#toRawObject(result),
|
|
serializationOptions,
|
|
resultOwnership,
|
|
realm,
|
|
{ seenNodeIds }
|
|
);
|
|
|
|
return {
|
|
evaluationStatus,
|
|
realmId: realm.id,
|
|
result: dataSuccess,
|
|
_extraData: { seenNodeIds },
|
|
};
|
|
}
|
|
case EvaluationStatus.Throw: {
|
|
const dataThrow = this.#buildExceptionDetails(
|
|
exception,
|
|
stack,
|
|
realm,
|
|
resultOwnership,
|
|
seenNodeIds
|
|
);
|
|
|
|
return {
|
|
evaluationStatus,
|
|
exceptionDetails: dataThrow,
|
|
realmId: realm.id,
|
|
_extraData: { seenNodeIds },
|
|
};
|
|
}
|
|
default:
|
|
throw new lazy.error.UnsupportedOperationError(
|
|
`Unsupported completion value for expression evaluation`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emit "script.message" event with provided data.
|
|
*
|
|
* @param {Realm} realm
|
|
* @param {ChannelProperties} channelProperties
|
|
* @param {RemoteValue} message
|
|
*/
|
|
#emitScriptMessage = (realm, channelProperties, message) => {
|
|
const {
|
|
channel,
|
|
ownership: ownershipType = lazy.OwnershipModel.None,
|
|
serializationOptions,
|
|
} = channelProperties;
|
|
|
|
const seenNodeIds = new Map();
|
|
const data = this.serialize(
|
|
this.#toRawObject(message),
|
|
lazy.setDefaultSerializationOptions(serializationOptions),
|
|
ownershipType,
|
|
realm,
|
|
{ seenNodeIds }
|
|
);
|
|
|
|
this.emitEvent("script.message", {
|
|
channel,
|
|
data,
|
|
source: this.#getSource(realm),
|
|
_extraData: { seenNodeIds },
|
|
});
|
|
};
|
|
|
|
#getSource(realm) {
|
|
return {
|
|
realm: realm.id,
|
|
context: this.messageHandler.context,
|
|
};
|
|
}
|
|
|
|
#toRawObject(maybeDebuggerObject) {
|
|
if (maybeDebuggerObject instanceof Debugger.Object) {
|
|
// Retrieve the referent for the provided Debugger.object.
|
|
// See https://firefox-source-docs.mozilla.org/devtools-user/debugger-api/debugger.object/index.html
|
|
const rawObject = maybeDebuggerObject.unsafeDereference();
|
|
|
|
// TODO: Getters for Maps and Sets iterators return "Opaque" objects and
|
|
// are not iterable. RemoteValue.sys.mjs' serializer should handle calling
|
|
// waiveXrays on Maps/Sets/... and then unwaiveXrays on entries but since
|
|
// we serialize with maxDepth=1, calling waiveXrays once on the root
|
|
// object allows to return correctly serialized values.
|
|
return Cu.waiveXrays(rawObject);
|
|
}
|
|
|
|
// If maybeDebuggerObject was not a Debugger.Object, it is a primitive value
|
|
// which can be used as is.
|
|
return maybeDebuggerObject;
|
|
}
|
|
|
|
/**
|
|
* Call a function in the current window global.
|
|
*
|
|
* @param {object} options
|
|
* @param {boolean} options.awaitPromise
|
|
* Determines if the command should wait for the return value of the
|
|
* expression to resolve, if this return value is a Promise.
|
|
* @param {Array<RemoteValue>=} options.commandArguments
|
|
* The arguments to pass to the function call.
|
|
* @param {string} options.functionDeclaration
|
|
* The body of the function to call.
|
|
* @param {string=} options.realmId
|
|
* The id of the realm.
|
|
* @param {OwnershipModel} options.resultOwnership
|
|
* The ownership model to use for the results of this evaluation.
|
|
* @param {string=} options.sandbox
|
|
* The name of the sandbox.
|
|
* @param {SerializationOptions=} options.serializationOptions
|
|
* An object which holds the information of how the result of evaluation
|
|
* in case of ECMAScript objects should be serialized.
|
|
* @param {RemoteValue=} options.thisParameter
|
|
* The value of the this keyword for the function call.
|
|
* @param {boolean=} options.userActivation
|
|
* Determines whether execution should be treated as initiated by user.
|
|
*
|
|
* @returns {object}
|
|
* - evaluationStatus {EvaluationStatus} One of "normal", "throw".
|
|
* - exceptionDetails {ExceptionDetails=} the details of the exception if
|
|
* the evaluation status was "throw".
|
|
* - result {RemoteValue=} the result of the evaluation serialized as a
|
|
* RemoteValue if the evaluation status was "normal".
|
|
*/
|
|
async callFunctionDeclaration(options) {
|
|
const {
|
|
awaitPromise,
|
|
commandArguments = null,
|
|
functionDeclaration,
|
|
realmId = null,
|
|
resultOwnership,
|
|
sandbox: sandboxName = null,
|
|
serializationOptions,
|
|
thisParameter = null,
|
|
userActivation,
|
|
} = options;
|
|
|
|
const realm = this.messageHandler.getRealm({ realmId, sandboxName });
|
|
|
|
const deserializedArguments =
|
|
commandArguments !== null
|
|
? commandArguments.map(arg =>
|
|
this.deserialize(arg, realm, {
|
|
emitScriptMessage: this.#emitScriptMessage,
|
|
})
|
|
)
|
|
: [];
|
|
|
|
const deserializedThis =
|
|
thisParameter !== null
|
|
? this.deserialize(thisParameter, realm, {
|
|
emitScriptMessage: this.#emitScriptMessage,
|
|
})
|
|
: null;
|
|
|
|
realm.userActivationEnabled = userActivation;
|
|
|
|
const rv = realm.executeInGlobalWithBindings(
|
|
functionDeclaration,
|
|
deserializedArguments,
|
|
deserializedThis
|
|
);
|
|
|
|
return this.#buildReturnValue(
|
|
rv,
|
|
realm,
|
|
awaitPromise,
|
|
resultOwnership,
|
|
serializationOptions
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Delete the provided handles from the realm corresponding to the provided
|
|
* sandbox name.
|
|
*
|
|
* @param {object=} options
|
|
* @param {Array<string>} options.handles
|
|
* Array of handle ids to disown.
|
|
* @param {string=} options.realmId
|
|
* The id of the realm.
|
|
* @param {string=} options.sandbox
|
|
* The name of the sandbox.
|
|
*/
|
|
disownHandles(options) {
|
|
const { handles, realmId = null, sandbox: sandboxName = null } = options;
|
|
const realm = this.messageHandler.getRealm({ realmId, sandboxName });
|
|
for (const handle of handles) {
|
|
realm.removeObjectHandle(handle);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Evaluate a provided expression in the current window global.
|
|
*
|
|
* @param {object} options
|
|
* @param {boolean} options.awaitPromise
|
|
* Determines if the command should wait for the return value of the
|
|
* expression to resolve, if this return value is a Promise.
|
|
* @param {string} options.expression
|
|
* The expression to evaluate.
|
|
* @param {string=} options.realmId
|
|
* The id of the realm.
|
|
* @param {OwnershipModel} options.resultOwnership
|
|
* The ownership model to use for the results of this evaluation.
|
|
* @param {string=} options.sandbox
|
|
* The name of the sandbox.
|
|
* @param {boolean=} options.userActivation
|
|
* Determines whether execution should be treated as initiated by user.
|
|
*
|
|
* @returns {object}
|
|
* - evaluationStatus {EvaluationStatus} One of "normal", "throw".
|
|
* - exceptionDetails {ExceptionDetails=} the details of the exception if
|
|
* the evaluation status was "throw".
|
|
* - result {RemoteValue=} the result of the evaluation serialized as a
|
|
* RemoteValue if the evaluation status was "normal".
|
|
*/
|
|
async evaluateExpression(options) {
|
|
const {
|
|
awaitPromise,
|
|
expression,
|
|
realmId = null,
|
|
resultOwnership,
|
|
sandbox: sandboxName = null,
|
|
serializationOptions,
|
|
userActivation,
|
|
} = options;
|
|
|
|
const realm = this.messageHandler.getRealm({ realmId, sandboxName });
|
|
|
|
realm.userActivationEnabled = userActivation;
|
|
|
|
const rv = realm.executeInGlobal(expression);
|
|
|
|
return this.#buildReturnValue(
|
|
rv,
|
|
realm,
|
|
awaitPromise,
|
|
resultOwnership,
|
|
serializationOptions
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get realms for the current window global.
|
|
*
|
|
* @returns {Array<object>}
|
|
* - context {BrowsingContext} The browsing context, associated with the realm.
|
|
* - origin {string} The serialization of an origin.
|
|
* - realm {string} The realm unique identifier.
|
|
* - sandbox {string=} The name of the sandbox.
|
|
* - type {RealmType.Window} The window realm type.
|
|
*/
|
|
getWindowRealms() {
|
|
return Array.from(this.messageHandler.realms.values()).map(realm => {
|
|
const { context, origin, realm: id, sandbox, type } = realm.getInfo();
|
|
return { context, origin, realm: id, sandbox, type };
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Internal commands
|
|
*/
|
|
|
|
_applySessionData() {}
|
|
|
|
/**
|
|
* Evaluate a provided list of preload scripts in the current window global.
|
|
*
|
|
* @param {object} options
|
|
* @param {Array<string>} options.scripts
|
|
* The list of scripts to evaluate.
|
|
*/
|
|
_evaluatePreloadScripts(options) {
|
|
const { scripts } = options;
|
|
|
|
for (const script of scripts) {
|
|
const {
|
|
arguments: commandArguments,
|
|
functionDeclaration,
|
|
sandbox,
|
|
} = script;
|
|
const realm = this.messageHandler.getRealm({ sandboxName: sandbox });
|
|
const deserializedArguments = commandArguments.map(arg =>
|
|
this.deserialize(arg, realm, {
|
|
emitScriptMessage: this.#emitScriptMessage,
|
|
})
|
|
);
|
|
const rv = realm.executeInGlobalWithBindings(
|
|
functionDeclaration,
|
|
deserializedArguments
|
|
);
|
|
|
|
if ("throw" in rv) {
|
|
const exception = this.#toRawObject(rv.throw);
|
|
realm.reportError(lazy.stringify(exception), rv.stack);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const script = ScriptModule;
|