script.sys.mjs (13715B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 11 getFramesFromStack: "chrome://remote/content/shared/Stack.sys.mjs", 12 isChromeFrame: "chrome://remote/content/shared/Stack.sys.mjs", 13 OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs", 14 setDefaultSerializationOptions: 15 "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs", 16 stringify: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs", 17 }); 18 19 /** 20 * @typedef {string} EvaluationStatus 21 */ 22 23 /** 24 * Enum of possible evaluation states. 25 * 26 * @readonly 27 * @enum {EvaluationStatus} 28 */ 29 const EvaluationStatus = { 30 Normal: "normal", 31 Throw: "throw", 32 }; 33 34 class ScriptModule extends WindowGlobalBiDiModule { 35 destroy() {} 36 37 #buildExceptionDetails( 38 exception, 39 stack, 40 realm, 41 resultOwnership, 42 seenNodeIds 43 ) { 44 exception = this.#toRawObject(exception); 45 46 // A stacktrace is mandatory to build exception details and a missing stack 47 // means we encountered an unexpected issue. Throw with an explicit error. 48 if (!stack) { 49 throw new Error( 50 `Missing stack, unable to build exceptionDetails for exception: ${lazy.stringify( 51 exception 52 )}` 53 ); 54 } 55 56 const frames = lazy.getFramesFromStack(stack) || []; 57 const callFrames = frames 58 // Remove chrome/internal frames 59 .filter(frame => !lazy.isChromeFrame(frame)) 60 // Translate frames from getFramesFromStack to frames expected by 61 // WebDriver BiDi. 62 .map(frame => { 63 return { 64 columnNumber: frame.columnNumber - 1, 65 functionName: frame.functionName, 66 lineNumber: frame.lineNumber - 1, 67 url: frame.filename, 68 }; 69 }); 70 71 return { 72 columnNumber: stack.column - 1, 73 exception: this.serialize( 74 exception, 75 lazy.setDefaultSerializationOptions(), 76 resultOwnership, 77 realm, 78 { seenNodeIds } 79 ), 80 lineNumber: stack.line - 1, 81 stackTrace: { callFrames }, 82 text: lazy.stringify(exception), 83 }; 84 } 85 86 async #buildReturnValue( 87 rv, 88 realm, 89 awaitPromise, 90 resultOwnership, 91 serializationOptions 92 ) { 93 let evaluationStatus, exception, result, stack; 94 95 if ("return" in rv) { 96 evaluationStatus = EvaluationStatus.Normal; 97 if ( 98 awaitPromise && 99 // Only non-primitive return values are wrapped in Debugger.Object. 100 rv.return instanceof Debugger.Object && 101 rv.return.isPromise 102 ) { 103 try { 104 // Force wrapping the promise resolution result in a Debugger.Object 105 // wrapper for consistency with the synchronous codepath. 106 const asyncResult = await rv.return.unsafeDereference(); 107 result = realm.globalObjectReference.makeDebuggeeValue(asyncResult); 108 } catch (asyncException) { 109 evaluationStatus = EvaluationStatus.Throw; 110 exception = 111 realm.globalObjectReference.makeDebuggeeValue(asyncException); 112 113 // If the returned promise was rejected by calling its reject callback 114 // the stack will be available on promiseResolutionSite. 115 // Otherwise, (eg. rejected Promise chained with a then() call) we 116 // fallback on the promiseAllocationSite. 117 stack = 118 rv.return.promiseResolutionSite || rv.return.promiseAllocationSite; 119 } 120 } else { 121 // rv.return is a Debugger.Object or a primitive. 122 result = rv.return; 123 } 124 } else if ("throw" in rv) { 125 // rv.throw will be set if the evaluation synchronously failed, either if 126 // the script contains a syntax error or throws an exception. 127 evaluationStatus = EvaluationStatus.Throw; 128 exception = rv.throw; 129 stack = rv.stack; 130 } 131 132 const seenNodeIds = new Map(); 133 switch (evaluationStatus) { 134 case EvaluationStatus.Normal: { 135 const dataSuccess = this.serialize( 136 this.#toRawObject(result), 137 serializationOptions, 138 resultOwnership, 139 realm, 140 { seenNodeIds } 141 ); 142 143 return { 144 evaluationStatus, 145 realmId: realm.id, 146 result: dataSuccess, 147 _extraData: { seenNodeIds }, 148 }; 149 } 150 case EvaluationStatus.Throw: { 151 const dataThrow = this.#buildExceptionDetails( 152 exception, 153 stack, 154 realm, 155 resultOwnership, 156 seenNodeIds 157 ); 158 159 return { 160 evaluationStatus, 161 exceptionDetails: dataThrow, 162 realmId: realm.id, 163 _extraData: { seenNodeIds }, 164 }; 165 } 166 default: 167 throw new lazy.error.UnsupportedOperationError( 168 `Unsupported completion value for expression evaluation` 169 ); 170 } 171 } 172 173 /** 174 * Emit "script.message" event with provided data. 175 * 176 * @param {Realm} realm 177 * @param {ChannelProperties} channelProperties 178 * @param {RemoteValue} message 179 */ 180 #emitScriptMessage = (realm, channelProperties, message) => { 181 const { 182 channel, 183 ownership: ownershipType = lazy.OwnershipModel.None, 184 serializationOptions, 185 } = channelProperties; 186 187 const seenNodeIds = new Map(); 188 const data = this.serialize( 189 this.#toRawObject(message), 190 lazy.setDefaultSerializationOptions(serializationOptions), 191 ownershipType, 192 realm, 193 { seenNodeIds } 194 ); 195 196 this.emitEvent("script.message", { 197 channel, 198 data, 199 source: this.#getSource(realm), 200 _extraData: { seenNodeIds }, 201 }); 202 }; 203 204 #getSource(realm) { 205 return { 206 realm: realm.id, 207 context: this.messageHandler.context, 208 }; 209 } 210 211 #toRawObject(maybeDebuggerObject) { 212 if (maybeDebuggerObject instanceof Debugger.Object) { 213 // Retrieve the referent for the provided Debugger.object. 214 // See https://firefox-source-docs.mozilla.org/devtools-user/debugger-api/debugger.object/index.html 215 const rawObject = maybeDebuggerObject.unsafeDereference(); 216 217 // TODO: Getters for Maps and Sets iterators return "Opaque" objects and 218 // are not iterable. RemoteValue.sys.mjs' serializer should handle calling 219 // waiveXrays on Maps/Sets/... and then unwaiveXrays on entries but since 220 // we serialize with maxDepth=1, calling waiveXrays once on the root 221 // object allows to return correctly serialized values. 222 return Cu.waiveXrays(rawObject); 223 } 224 225 // If maybeDebuggerObject was not a Debugger.Object, it is a primitive value 226 // which can be used as is. 227 return maybeDebuggerObject; 228 } 229 230 /** 231 * Call a function in the current window global. 232 * 233 * @param {object} options 234 * @param {boolean} options.awaitPromise 235 * Determines if the command should wait for the return value of the 236 * expression to resolve, if this return value is a Promise. 237 * @param {Array<RemoteValue>=} options.commandArguments 238 * The arguments to pass to the function call. 239 * @param {string} options.functionDeclaration 240 * The body of the function to call. 241 * @param {string=} options.realmId 242 * The id of the realm. 243 * @param {OwnershipModel} options.resultOwnership 244 * The ownership model to use for the results of this evaluation. 245 * @param {string=} options.sandbox 246 * The name of the sandbox. 247 * @param {SerializationOptions=} options.serializationOptions 248 * An object which holds the information of how the result of evaluation 249 * in case of ECMAScript objects should be serialized. 250 * @param {RemoteValue=} options.thisParameter 251 * The value of the this keyword for the function call. 252 * @param {boolean=} options.userActivation 253 * Determines whether execution should be treated as initiated by user. 254 * 255 * @returns {object} 256 * - evaluationStatus {EvaluationStatus} One of "normal", "throw". 257 * - exceptionDetails {ExceptionDetails=} the details of the exception if 258 * the evaluation status was "throw". 259 * - result {RemoteValue=} the result of the evaluation serialized as a 260 * RemoteValue if the evaluation status was "normal". 261 */ 262 async callFunctionDeclaration(options) { 263 const { 264 awaitPromise, 265 commandArguments = null, 266 functionDeclaration, 267 realmId = null, 268 resultOwnership, 269 sandbox: sandboxName = null, 270 serializationOptions, 271 thisParameter = null, 272 userActivation, 273 } = options; 274 275 const realm = this.messageHandler.getRealm({ realmId, sandboxName }); 276 277 const deserializedArguments = 278 commandArguments !== null 279 ? commandArguments.map(arg => 280 this.deserialize(arg, realm, { 281 emitScriptMessage: this.#emitScriptMessage, 282 }) 283 ) 284 : []; 285 286 const deserializedThis = 287 thisParameter !== null 288 ? this.deserialize(thisParameter, realm, { 289 emitScriptMessage: this.#emitScriptMessage, 290 }) 291 : null; 292 293 realm.userActivationEnabled = userActivation; 294 295 const rv = realm.executeInGlobalWithBindings( 296 functionDeclaration, 297 deserializedArguments, 298 deserializedThis 299 ); 300 301 return this.#buildReturnValue( 302 rv, 303 realm, 304 awaitPromise, 305 resultOwnership, 306 serializationOptions 307 ); 308 } 309 310 /** 311 * Delete the provided handles from the realm corresponding to the provided 312 * sandbox name. 313 * 314 * @param {object=} options 315 * @param {Array<string>} options.handles 316 * Array of handle ids to disown. 317 * @param {string=} options.realmId 318 * The id of the realm. 319 * @param {string=} options.sandbox 320 * The name of the sandbox. 321 */ 322 disownHandles(options) { 323 const { handles, realmId = null, sandbox: sandboxName = null } = options; 324 const realm = this.messageHandler.getRealm({ realmId, sandboxName }); 325 for (const handle of handles) { 326 realm.removeObjectHandle(handle); 327 } 328 } 329 330 /** 331 * Evaluate a provided expression in the current window global. 332 * 333 * @param {object} options 334 * @param {boolean} options.awaitPromise 335 * Determines if the command should wait for the return value of the 336 * expression to resolve, if this return value is a Promise. 337 * @param {string} options.expression 338 * The expression to evaluate. 339 * @param {string=} options.realmId 340 * The id of the realm. 341 * @param {OwnershipModel} options.resultOwnership 342 * The ownership model to use for the results of this evaluation. 343 * @param {string=} options.sandbox 344 * The name of the sandbox. 345 * @param {boolean=} options.userActivation 346 * Determines whether execution should be treated as initiated by user. 347 * 348 * @returns {object} 349 * - evaluationStatus {EvaluationStatus} One of "normal", "throw". 350 * - exceptionDetails {ExceptionDetails=} the details of the exception if 351 * the evaluation status was "throw". 352 * - result {RemoteValue=} the result of the evaluation serialized as a 353 * RemoteValue if the evaluation status was "normal". 354 */ 355 async evaluateExpression(options) { 356 const { 357 awaitPromise, 358 expression, 359 realmId = null, 360 resultOwnership, 361 sandbox: sandboxName = null, 362 serializationOptions, 363 userActivation, 364 } = options; 365 366 const realm = this.messageHandler.getRealm({ realmId, sandboxName }); 367 368 realm.userActivationEnabled = userActivation; 369 370 const rv = realm.executeInGlobal(expression); 371 372 return this.#buildReturnValue( 373 rv, 374 realm, 375 awaitPromise, 376 resultOwnership, 377 serializationOptions 378 ); 379 } 380 381 /** 382 * Get realms for the current window global. 383 * 384 * @returns {Array<object>} 385 * - context {BrowsingContext} The browsing context, associated with the realm. 386 * - origin {string} The serialization of an origin. 387 * - realm {string} The realm unique identifier. 388 * - sandbox {string=} The name of the sandbox. 389 * - type {RealmType.Window} The window realm type. 390 */ 391 getWindowRealms() { 392 return Array.from(this.messageHandler.realms.values()).map(realm => { 393 const { context, origin, realm: id, sandbox, type } = realm.getInfo(); 394 return { context, origin, realm: id, sandbox, type }; 395 }); 396 } 397 398 /** 399 * Internal commands 400 */ 401 402 _applySessionData() {} 403 404 /** 405 * Evaluate a provided list of preload scripts in the current window global. 406 * 407 * @param {object} options 408 * @param {Array<string>} options.scripts 409 * The list of scripts to evaluate. 410 */ 411 _evaluatePreloadScripts(options) { 412 const { scripts } = options; 413 414 for (const script of scripts) { 415 const { 416 arguments: commandArguments, 417 functionDeclaration, 418 sandbox, 419 } = script; 420 const realm = this.messageHandler.getRealm({ sandboxName: sandbox }); 421 const deserializedArguments = commandArguments.map(arg => 422 this.deserialize(arg, realm, { 423 emitScriptMessage: this.#emitScriptMessage, 424 }) 425 ); 426 const rv = realm.executeInGlobalWithBindings( 427 functionDeclaration, 428 deserializedArguments 429 ); 430 431 if ("throw" in rv) { 432 const exception = this.#toRawObject(rv.throw); 433 realm.reportError(lazy.stringify(exception), rv.stack); 434 } 435 } 436 } 437 } 438 439 export const script = ScriptModule;