script.sys.mjs (33078B)
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 { RootBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/RootBiDiModule.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", 11 BrowsingContextListener: 12 "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs", 13 ContextDescriptorType: 14 "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs", 15 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 16 generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", 17 NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs", 18 OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs", 19 pprint: "chrome://remote/content/shared/Format.sys.mjs", 20 processExtraData: 21 "chrome://remote/content/webdriver-bidi/modules/Intercept.sys.mjs", 22 RealmType: "chrome://remote/content/shared/Realm.sys.mjs", 23 SessionDataMethod: 24 "chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs", 25 setDefaultAndAssertSerializationOptions: 26 "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs", 27 UserContextManager: 28 "chrome://remote/content/shared/UserContextManager.sys.mjs", 29 WindowGlobalMessageHandler: 30 "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs", 31 }); 32 33 /** 34 * @typedef {string} ScriptEvaluateResultType 35 */ 36 37 /** 38 * Enum of possible evaluation result types. 39 * 40 * @readonly 41 * @enum {ScriptEvaluateResultType} 42 */ 43 const ScriptEvaluateResultType = { 44 Exception: "exception", 45 Success: "success", 46 }; 47 48 /** 49 * An object that holds information about the preload script. 50 * 51 * @typedef PreloadScript 52 * 53 * @property {Array<ChannelValue>=} arguments 54 * The arguments to pass to the function call. 55 * @property {Array<string>=} navigables 56 * The list of navigable browser ids where 57 * the preload script should be executed. 58 * @property {string} functionDeclaration 59 * The expression to evaluate. 60 * @property {string=} sandbox 61 * The name of the sandbox. 62 * @property {Array<string>=} userContexts 63 * The list of internal user context ids where 64 * the preload script should be executed. 65 */ 66 67 class ScriptModule extends RootBiDiModule { 68 #contextListener; 69 #preloadScriptMap; 70 #realmInfoMap; 71 #subscribedEvents; 72 73 constructor(messageHandler) { 74 super(messageHandler); 75 76 this.#contextListener = new lazy.BrowsingContextListener(); 77 this.#contextListener.on("attached", this.#onContextAttached); 78 79 // Map in which the keys are UUIDs, and the values are structs 80 // of the type PreloadScript. 81 this.#preloadScriptMap = new Map(); 82 83 // Map with browsing contexts as keys and realm info object 84 // as values. 85 this.#realmInfoMap = new WeakMap(); 86 87 // Set of event names which have active subscriptions. 88 this.#subscribedEvents = new Set(); 89 } 90 91 destroy() { 92 this.#contextListener.off("attached", this.#onContextAttached); 93 this.#contextListener.destroy(); 94 95 this.#preloadScriptMap = null; 96 this.#realmInfoMap = null; 97 this.#subscribedEvents = null; 98 } 99 100 /** 101 * Used as return value for script.addPreloadScript command. 102 * 103 * @typedef AddPreloadScriptResult 104 * 105 * @property {string} script 106 * The unique id associated with added preload script. 107 */ 108 109 /** 110 * @typedef ChannelProperties 111 * 112 * @property {string} channel 113 * The channel id. 114 * @property {SerializationOptions=} serializationOptions 115 * An object which holds the information of how the result of evaluation 116 * in case of ECMAScript objects should be serialized. 117 * @property {OwnershipModel=} ownership 118 * The ownership model to use for the results of this evaluation. Defaults 119 * to `OwnershipModel.None`. 120 */ 121 122 /** 123 * Represents a channel used to send custom messages from preload script 124 * to clients. 125 * 126 * @typedef ChannelValue 127 * 128 * @property {'channel'} type 129 * @property {ChannelProperties} value 130 */ 131 132 /** 133 * Adds a preload script, which runs on creation of a new Window, 134 * before any author-defined script have run. 135 * 136 * @param {object=} options 137 * @param {Array<ChannelValue>=} options.arguments 138 * The arguments to pass to the function call. 139 * @param {Array<string>=} options.contexts 140 * The list of the browsing context ids. 141 * @param {string} options.functionDeclaration 142 * The expression to evaluate. 143 * @param {string=} options.sandbox 144 * The name of the sandbox. If the value is null or empty 145 * string, the default realm will be used. 146 * @param {Array<string>=} options.userContexts 147 * The list of the user context ids. 148 * 149 * @returns {AddPreloadScriptResult} 150 * 151 * @throws {InvalidArgumentError} 152 * If any of the arguments does not have the expected type. 153 */ 154 async addPreloadScript(options = {}) { 155 const { 156 arguments: commandArguments = [], 157 contexts: contextIds = null, 158 functionDeclaration, 159 sandbox = null, 160 userContexts: userContextIds = null, 161 } = options; 162 let userContexts = null; 163 let navigables = null; 164 165 if (contextIds !== null) { 166 lazy.assert.isNonEmptyArray( 167 contextIds, 168 lazy.pprint`Expected "contexts" to be a non-empty array, got ${contextIds}` 169 ); 170 171 for (const contextId of contextIds) { 172 lazy.assert.string( 173 contextId, 174 lazy.pprint`Expected elements of "contexts" to be a string, got ${contextId}` 175 ); 176 } 177 } else if (userContextIds !== null) { 178 lazy.assert.isNonEmptyArray( 179 userContextIds, 180 lazy.pprint`Expected "userContextIds" to be a non-empty array, got ${userContextIds}` 181 ); 182 183 for (const userContextId of userContextIds) { 184 lazy.assert.string( 185 userContextId, 186 lazy.pprint`Expected elements of "userContexts" to be a string, got ${userContextId}` 187 ); 188 } 189 } 190 191 lazy.assert.string( 192 functionDeclaration, 193 lazy.pprint`Expected "functionDeclaration" to be a string, got ${functionDeclaration}` 194 ); 195 196 if (sandbox != null) { 197 lazy.assert.string( 198 sandbox, 199 lazy.pprint`Expected "sandbox" to be a string, got ${sandbox}` 200 ); 201 } 202 203 lazy.assert.array( 204 commandArguments, 205 lazy.pprint`Expected "arguments" to be an array, got ${commandArguments}` 206 ); 207 208 commandArguments.forEach(({ type, value }) => { 209 lazy.assert.that( 210 t => t === "channel", 211 lazy.pprint`Expected argument "type" to be "channel", got ${type}` 212 )(type); 213 this.#assertChannelArgument(value); 214 }); 215 216 if (contextIds !== null && userContextIds !== null) { 217 throw new lazy.error.InvalidArgumentError( 218 `Providing both "contexts" and "userContexts" arguments is not supported` 219 ); 220 } 221 222 if (contextIds !== null) { 223 navigables = new Set(); 224 225 for (const contextId of contextIds) { 226 const context = this._getNavigable(contextId); 227 228 lazy.assert.topLevel( 229 context, 230 lazy.pprint`Browsing context with id ${contextId} is not top-level` 231 ); 232 233 navigables.add(context.browserId); 234 } 235 } else if (userContextIds !== null) { 236 userContexts = new Set(); 237 238 for (const userContextId of userContextIds) { 239 const internalId = 240 lazy.UserContextManager.getInternalIdById(userContextId); 241 242 if (internalId === null) { 243 throw new lazy.error.NoSuchUserContextError( 244 `User context with id: ${userContextId} doesn't exist` 245 ); 246 } 247 248 userContexts.add(internalId); 249 } 250 } 251 252 const script = lazy.generateUUID(); 253 const preloadScript = { 254 arguments: commandArguments, 255 contexts: navigables, 256 functionDeclaration, 257 sandbox, 258 userContexts, 259 }; 260 261 this.#preloadScriptMap.set(script, preloadScript); 262 263 const preloadScriptDataItem = { 264 category: "preload-script", 265 moduleName: "_configuration", 266 values: [ 267 { 268 ...preloadScript, 269 script, 270 }, 271 ], 272 }; 273 274 if (navigables === null && userContexts === null) { 275 await this.messageHandler.addSessionDataItem({ 276 ...preloadScriptDataItem, 277 contextDescriptor: { 278 type: lazy.ContextDescriptorType.All, 279 }, 280 }); 281 } else { 282 const preloadScriptDataItems = []; 283 284 if (navigables === null) { 285 for (const id of userContexts) { 286 preloadScriptDataItems.push({ 287 ...preloadScriptDataItem, 288 contextDescriptor: { 289 type: lazy.ContextDescriptorType.UserContext, 290 id, 291 }, 292 method: lazy.SessionDataMethod.Add, 293 }); 294 } 295 } else { 296 for (const id of navigables) { 297 preloadScriptDataItems.push({ 298 ...preloadScriptDataItem, 299 contextDescriptor: { 300 type: lazy.ContextDescriptorType.TopBrowsingContext, 301 id, 302 }, 303 method: lazy.SessionDataMethod.Add, 304 }); 305 } 306 } 307 308 await this.messageHandler.updateSessionData(preloadScriptDataItems); 309 } 310 311 return { script }; 312 } 313 314 /** 315 * Used to represent a frame of a JavaScript stack trace. 316 * 317 * @typedef StackFrame 318 * 319 * @property {number} columnNumber 320 * @property {string} functionName 321 * @property {number} lineNumber 322 * @property {string} url 323 */ 324 325 /** 326 * Used to represent a JavaScript stack at a point in script execution. 327 * 328 * @typedef StackTrace 329 * 330 * @property {Array<StackFrame>} callFrames 331 */ 332 333 /** 334 * Used to represent a JavaScript exception. 335 * 336 * @typedef ExceptionDetails 337 * 338 * @property {number} columnNumber 339 * @property {RemoteValue} exception 340 * @property {number} lineNumber 341 * @property {StackTrace} stackTrace 342 * @property {string} text 343 */ 344 345 /** 346 * Used as return value for script.evaluate, as one of the available variants 347 * {ScriptEvaluateResultException} or {ScriptEvaluateResultSuccess}. 348 * 349 * @typedef ScriptEvaluateResult 350 */ 351 352 /** 353 * Used as return value for script.evaluate when the script completes with a 354 * thrown exception. 355 * 356 * @typedef ScriptEvaluateResultException 357 * 358 * @property {ExceptionDetails} exceptionDetails 359 * @property {string} realm 360 * @property {ScriptEvaluateResultType} [type=ScriptEvaluateResultType.Exception] 361 */ 362 363 /** 364 * Used as return value for script.evaluate when the script completes 365 * normally. 366 * 367 * @typedef ScriptEvaluateResultSuccess 368 * 369 * @property {string} realm 370 * @property {RemoteValue} result 371 * @property {ScriptEvaluateResultType} [type=ScriptEvaluateResultType.Success] 372 */ 373 374 /** 375 * Calls a provided function with given arguments and scope in the provided 376 * target, which is either a realm or a browsing context. 377 * 378 * @param {object=} options 379 * @param {Array<RemoteValue>=} options.arguments 380 * The arguments to pass to the function call. 381 * @param {boolean} options.awaitPromise 382 * Determines if the command should wait for the return value of the 383 * expression to resolve, if this return value is a Promise. 384 * @param {string} options.functionDeclaration 385 * The expression to evaluate. 386 * @param {OwnershipModel=} options.resultOwnership 387 * The ownership model to use for the results of this evaluation. Defaults 388 * to `OwnershipModel.None`. 389 * @param {SerializationOptions=} options.serializationOptions 390 * An object which holds the information of how the result of evaluation 391 * in case of ECMAScript objects should be serialized. 392 * @param {object} options.target 393 * The target for the evaluation, which either matches the definition for 394 * a RealmTarget or for ContextTarget. 395 * @param {RemoteValue=} options.this 396 * The value of the this keyword for the function call. 397 * @param {boolean=} options.userActivation 398 * Determines whether execution should be treated as initiated by user. 399 * Defaults to `false`. 400 * 401 * @returns {ScriptEvaluateResult} 402 * 403 * @throws {InvalidArgumentError} 404 * If any of the arguments does not have the expected type. 405 * @throws {NoSuchFrameError} 406 * If the target cannot be found. 407 */ 408 async callFunction(options = {}) { 409 const { 410 arguments: commandArguments = null, 411 awaitPromise, 412 functionDeclaration, 413 resultOwnership = lazy.OwnershipModel.None, 414 serializationOptions, 415 target = {}, 416 this: thisParameter = null, 417 userActivation = false, 418 } = options; 419 420 lazy.assert.string( 421 functionDeclaration, 422 lazy.pprint`Expected "functionDeclaration" to be a string, got ${functionDeclaration}` 423 ); 424 425 lazy.assert.boolean( 426 awaitPromise, 427 lazy.pprint`Expected "awaitPromise" to be a boolean, got ${awaitPromise}` 428 ); 429 430 lazy.assert.boolean( 431 userActivation, 432 lazy.pprint`Expected "userActivation" to be a boolean, got ${userActivation}` 433 ); 434 435 this.#assertResultOwnership(resultOwnership); 436 437 if (commandArguments != null) { 438 lazy.assert.array( 439 commandArguments, 440 lazy.pprint`Expected "arguments" to be an array, got ${commandArguments}` 441 ); 442 commandArguments.forEach(({ type, value }) => { 443 if (type === "channel") { 444 this.#assertChannelArgument(value); 445 } 446 }); 447 } 448 449 const { contextId, realmId, sandbox } = this.#assertTarget(target); 450 const context = await this.#getContextFromTarget({ 451 contextId, 452 realmId, 453 supportsChromeScope: true, 454 }); 455 456 const serializationOptionsWithDefaults = 457 lazy.setDefaultAndAssertSerializationOptions(serializationOptions); 458 459 const evaluationResult = await this._forwardToWindowGlobal( 460 "callFunctionDeclaration", 461 context.id, 462 { 463 awaitPromise, 464 commandArguments, 465 functionDeclaration, 466 realmId, 467 resultOwnership, 468 sandbox, 469 serializationOptions: serializationOptionsWithDefaults, 470 thisParameter, 471 userActivation, 472 } 473 ); 474 475 return this.#buildReturnValue(evaluationResult); 476 } 477 478 /** 479 * The script.disown command disowns the given handles. This does not 480 * guarantee the handled object will be garbage collected, as there can be 481 * other handles or strong ECMAScript references. 482 * 483 * @param {object=} options 484 * @param {Array<string>} options.handles 485 * Array of handle ids to disown. 486 * @param {object} options.target 487 * The target owning the handles, which either matches the definition for 488 * a RealmTarget or for ContextTarget. 489 */ 490 async disown(options = {}) { 491 const { handles, target = {} } = options; 492 493 lazy.assert.array( 494 handles, 495 lazy.pprint`Expected "handles" to be an array, got ${handles}` 496 ); 497 handles.forEach(handle => { 498 lazy.assert.string( 499 handle, 500 lazy.pprint`Expected "handles" to be an array of strings, got ${handle}` 501 ); 502 }); 503 504 const { contextId, realmId, sandbox } = this.#assertTarget(target); 505 const context = await this.#getContextFromTarget({ contextId, realmId }); 506 await this._forwardToWindowGlobal("disownHandles", context.id, { 507 handles, 508 realmId, 509 sandbox, 510 }); 511 } 512 513 /** 514 * Evaluate a provided expression in the provided target, which is either a 515 * realm or a browsing context. 516 * 517 * @param {object=} options 518 * @param {boolean} options.awaitPromise 519 * Determines if the command should wait for the return value of the 520 * expression to resolve, if this return value is a Promise. 521 * @param {string} options.expression 522 * The expression to evaluate. 523 * @param {OwnershipModel=} options.resultOwnership 524 * The ownership model to use for the results of this evaluation. Defaults 525 * to `OwnershipModel.None`. 526 * @param {SerializationOptions=} options.serializationOptions 527 * An object which holds the information of how the result of evaluation 528 * in case of ECMAScript objects should be serialized. 529 * @param {object} options.target 530 * The target for the evaluation, which either matches the definition for 531 * a RealmTarget or for ContextTarget. 532 * @param {boolean=} options.userActivation 533 * Determines whether execution should be treated as initiated by user. 534 * Defaults to `false`. 535 * 536 * @returns {ScriptEvaluateResult} 537 * 538 * @throws {InvalidArgumentError} 539 * If any of the arguments does not have the expected type. 540 * @throws {NoSuchFrameError} 541 * If the target cannot be found. 542 */ 543 async evaluate(options = {}) { 544 const { 545 awaitPromise, 546 expression: source, 547 resultOwnership = lazy.OwnershipModel.None, 548 serializationOptions, 549 target = {}, 550 userActivation = false, 551 } = options; 552 553 lazy.assert.string( 554 source, 555 lazy.pprint`Expected "expression" to be a string, got ${source}` 556 ); 557 558 lazy.assert.boolean( 559 awaitPromise, 560 lazy.pprint`Expected "awaitPromise" to be a boolean, got ${awaitPromise}` 561 ); 562 563 lazy.assert.boolean( 564 userActivation, 565 lazy.pprint`Expected "userActivation" to be a boolean, got ${userActivation}` 566 ); 567 568 this.#assertResultOwnership(resultOwnership); 569 570 const { contextId, realmId, sandbox } = this.#assertTarget(target); 571 const context = await this.#getContextFromTarget({ 572 contextId, 573 realmId, 574 supportsChromeScope: true, 575 }); 576 577 const serializationOptionsWithDefaults = 578 lazy.setDefaultAndAssertSerializationOptions(serializationOptions); 579 580 const evaluationResult = await this._forwardToWindowGlobal( 581 "evaluateExpression", 582 context.id, 583 { 584 awaitPromise, 585 expression: source, 586 realmId, 587 resultOwnership, 588 sandbox, 589 serializationOptions: serializationOptionsWithDefaults, 590 userActivation, 591 } 592 ); 593 594 return this.#buildReturnValue(evaluationResult); 595 } 596 597 /** 598 * An object that holds basic information about a realm. 599 * 600 * @typedef BaseRealmInfo 601 * 602 * @property {string} id 603 * The realm unique identifier. 604 * @property {string} origin 605 * The serialization of an origin. 606 */ 607 608 /** 609 * 610 * @typedef WindowRealmInfoProperties 611 * 612 * @property {string} context 613 * The browsing context id, associated with the realm. 614 * @property {string=} sandbox 615 * The name of the sandbox. If the value is null or empty 616 * string, the default realm will be returned. 617 * @property {RealmType.Window} type 618 * The window realm type. 619 */ 620 621 /* eslint-disable jsdoc/valid-types */ 622 /** 623 * An object that holds information about a window realm. 624 * 625 * @typedef {BaseRealmInfo & WindowRealmInfoProperties} WindowRealmInfo 626 */ 627 /* eslint-enable jsdoc/valid-types */ 628 629 /** 630 * An object that holds information about a realm. 631 * 632 * @typedef {WindowRealmInfo} RealmInfo 633 */ 634 635 /** 636 * An object that holds a list of realms. 637 * 638 * @typedef ScriptGetRealmsResult 639 * 640 * @property {Array<RealmInfo>} realms 641 * List of realms. 642 */ 643 644 /** 645 * Returns a list of all realms, optionally filtered to realms 646 * of a specific type, or to the realms associated with 647 * a specified browsing context. 648 * 649 * @param {object=} options 650 * @param {string=} options.context 651 * The id of the browsing context to filter 652 * only realms associated with it. If not provided, return realms 653 * associated with all browsing contexts. 654 * @param {RealmType=} options.type 655 * Type of realm to filter. 656 * If not provided, return realms of all types. 657 * 658 * @returns {ScriptGetRealmsResult} 659 * 660 * @throws {InvalidArgumentError} 661 * If any of the arguments does not have the expected type. 662 * @throws {NoSuchFrameError} 663 * If the context cannot be found. 664 */ 665 async getRealms(options = {}) { 666 const { context: contextId = null, type = null } = options; 667 const destination = {}; 668 669 if (contextId !== null) { 670 lazy.assert.string( 671 contextId, 672 lazy.pprint`Expected "context" to be a string, got ${contextId}` 673 ); 674 destination.id = this._getNavigable(contextId).id; 675 } else { 676 destination.contextDescriptor = { 677 type: lazy.ContextDescriptorType.All, 678 }; 679 } 680 681 if (type !== null) { 682 const supportedRealmTypes = Object.values(lazy.RealmType); 683 if (!supportedRealmTypes.includes(type)) { 684 throw new lazy.error.InvalidArgumentError( 685 `Expected "type" to be one of ${supportedRealmTypes}, got ${type}` 686 ); 687 } 688 689 // Remove this check when other realm types are supported 690 if (type !== lazy.RealmType.Window) { 691 throw new lazy.error.UnsupportedOperationError( 692 `Unsupported "type": ${type}. Only "type" ${lazy.RealmType.Window} is currently supported.` 693 ); 694 } 695 } 696 697 return { realms: await this.#getRealmInfos(destination) }; 698 } 699 700 /** 701 * Removes a preload script. 702 * 703 * @param {object=} options 704 * @param {string} options.script 705 * The unique id associated with a preload script. 706 * 707 * @throws {InvalidArgumentError} 708 * If any of the arguments does not have the expected type. 709 * @throws {NoSuchScriptError} 710 * If the script cannot be found. 711 */ 712 async removePreloadScript(options = {}) { 713 const { script } = options; 714 715 lazy.assert.string( 716 script, 717 lazy.pprint`Expected "script" to be a string, got ${script}` 718 ); 719 720 if (!this.#preloadScriptMap.has(script)) { 721 throw new lazy.error.NoSuchScriptError( 722 `Preload script with id ${script} not found` 723 ); 724 } 725 726 const preloadScript = this.#preloadScriptMap.get(script); 727 const sessionDataItem = { 728 category: "preload-script", 729 moduleName: "_configuration", 730 values: [ 731 { 732 ...preloadScript, 733 script, 734 }, 735 ], 736 }; 737 738 if ( 739 preloadScript.contexts === null && 740 preloadScript.userContexts === null 741 ) { 742 await this.messageHandler.removeSessionDataItem({ 743 ...sessionDataItem, 744 contextDescriptor: { 745 type: lazy.ContextDescriptorType.All, 746 }, 747 }); 748 } else { 749 const sessionDataItemToUpdate = []; 750 751 if (preloadScript.contexts === null) { 752 for (const id of preloadScript.userContexts) { 753 sessionDataItemToUpdate.push({ 754 ...sessionDataItem, 755 contextDescriptor: { 756 type: lazy.ContextDescriptorType.UserContext, 757 id, 758 }, 759 method: lazy.SessionDataMethod.Remove, 760 }); 761 } 762 } else { 763 for (const id of preloadScript.contexts) { 764 sessionDataItemToUpdate.push({ 765 ...sessionDataItem, 766 contextDescriptor: { 767 type: lazy.ContextDescriptorType.TopBrowsingContext, 768 id, 769 }, 770 method: lazy.SessionDataMethod.Remove, 771 }); 772 } 773 } 774 775 await this.messageHandler.updateSessionData(sessionDataItemToUpdate); 776 } 777 778 this.#preloadScriptMap.delete(script); 779 } 780 781 #assertChannelArgument(value) { 782 lazy.assert.object( 783 value, 784 lazy.pprint`Expected channel argument to be an object, got ${value}` 785 ); 786 const { 787 channel, 788 ownership = lazy.OwnershipModel.None, 789 serializationOptions, 790 } = value; 791 lazy.assert.string( 792 channel, 793 lazy.pprint`Expected channel argument "channel" to be a string, got ${channel}` 794 ); 795 lazy.setDefaultAndAssertSerializationOptions(serializationOptions); 796 lazy.assert.that( 797 ownershipValue => 798 [lazy.OwnershipModel.None, lazy.OwnershipModel.Root].includes( 799 ownershipValue 800 ), 801 `Expected channel argument "ownership" to be one of ${Object.values( 802 lazy.OwnershipModel 803 )}, ` + lazy.pprint`got ${ownership}` 804 )(ownership); 805 806 return true; 807 } 808 809 #assertResultOwnership(resultOwnership) { 810 if ( 811 ![lazy.OwnershipModel.None, lazy.OwnershipModel.Root].includes( 812 resultOwnership 813 ) 814 ) { 815 throw new lazy.error.InvalidArgumentError( 816 `Expected "resultOwnership" to be one of ${Object.values( 817 lazy.OwnershipModel 818 )}, ` + lazy.pprint`got ${resultOwnership}` 819 ); 820 } 821 } 822 823 #assertTarget(target) { 824 lazy.assert.object( 825 target, 826 lazy.pprint`Expected "target" to be an object, got ${target}` 827 ); 828 829 const { context: contextId = null, sandbox = null } = target; 830 let { realm: realmId = null } = target; 831 832 if (contextId != null) { 833 lazy.assert.string( 834 contextId, 835 lazy.pprint`Expected target "context" to be a string, got ${contextId}` 836 ); 837 838 if (sandbox != null) { 839 lazy.assert.string( 840 sandbox, 841 lazy.pprint`Expected target "sandbox" to be a string, got ${sandbox}` 842 ); 843 } 844 845 // Ignore realm if context is provided. 846 realmId = null; 847 } else if (realmId != null) { 848 lazy.assert.string( 849 realmId, 850 lazy.pprint`Expected target "realm" to be a string, got ${realmId}` 851 ); 852 } else { 853 throw new lazy.error.InvalidArgumentError(`No context or realm provided`); 854 } 855 856 return { contextId, realmId, sandbox }; 857 } 858 859 #buildReturnValue(evaluationResult) { 860 evaluationResult = lazy.processExtraData( 861 this.messageHandler.sessionId, 862 evaluationResult 863 ); 864 865 const rv = { realm: evaluationResult.realmId }; 866 switch (evaluationResult.evaluationStatus) { 867 // TODO: Compare with EvaluationStatus.Normal after Bug 1774444 is fixed. 868 case "normal": 869 rv.type = ScriptEvaluateResultType.Success; 870 rv.result = evaluationResult.result; 871 break; 872 // TODO: Compare with EvaluationStatus.Throw after Bug 1774444 is fixed. 873 case "throw": 874 rv.type = ScriptEvaluateResultType.Exception; 875 rv.exceptionDetails = evaluationResult.exceptionDetails; 876 break; 877 default: 878 throw new lazy.error.UnsupportedOperationError( 879 `Unsupported evaluation status ${evaluationResult.evaluationStatus}` 880 ); 881 } 882 return rv; 883 } 884 885 async #getContextFromTarget({ 886 contextId, 887 realmId, 888 supportsChromeScope = false, 889 }) { 890 if (contextId !== null) { 891 return this._getNavigable(contextId, { supportsChromeScope }); 892 } 893 894 const destination = { 895 contextDescriptor: { 896 type: lazy.ContextDescriptorType.All, 897 }, 898 }; 899 const realmInfos = await this.#getRealmInfos(destination); 900 const realm = realmInfos.find(info => info.realm == realmId); 901 902 if (realm && realm.context !== null) { 903 return this._getNavigable(realm.context, { supportsChromeScope }); 904 } 905 906 throw new lazy.error.NoSuchFrameError(`Realm with id ${realmId} not found`); 907 } 908 909 async #getRealmInfos(destination) { 910 let realms = await this.messageHandler.forwardCommand({ 911 moduleName: "script", 912 commandName: "getWindowRealms", 913 destination: { 914 type: lazy.WindowGlobalMessageHandler.type, 915 ...destination, 916 }, 917 retryOnAbort: true, 918 }); 919 920 const isBroadcast = !!destination.contextDescriptor; 921 if (!isBroadcast) { 922 realms = [realms]; 923 } 924 925 return realms 926 .flat() 927 .map(realm => { 928 // Resolve browsing context to a TabManager id. 929 realm.context = lazy.NavigableManager.getIdForBrowsingContext( 930 realm.context 931 ); 932 return realm; 933 }) 934 .filter(realm => realm.context !== null); 935 } 936 937 #hasEventSubscriptionToContextCreated(browsingContext) { 938 const sessionData = 939 this.messageHandler.sessionData.getSessionDataForContext( 940 "browsingContext", 941 "event", 942 browsingContext 943 ); 944 945 return sessionData.some( 946 item => item.value === "browsingContext.contextCreated" 947 ); 948 } 949 950 #onContextAttached = (eventName, data) => { 951 const { browsingContext } = data; 952 // If there is a subscription for "browsingContext.contextCreated" event, 953 // do not send "script.realmCreated" event yet and 954 // wait until the "browsingContext.contextCreated" event is submitted 955 if ( 956 this.#realmInfoMap.has(browsingContext) && 957 !this.#hasEventSubscriptionToContextCreated(browsingContext) 958 ) { 959 this.#sendDelayedRealmCreatedEvent(browsingContext); 960 } 961 }; 962 963 #onContextCreatedSubmitted = (eventName, { browsingContext }) => { 964 if (this.#realmInfoMap.has(browsingContext)) { 965 this.#sendDelayedRealmCreatedEvent(browsingContext); 966 } 967 }; 968 969 #onRealmCreated = (eventName, { realmInfo }) => { 970 // Resolve browsing context to a TabManager id. 971 const context = lazy.NavigableManager.getIdForBrowsingContext( 972 realmInfo.context 973 ); 974 975 // Do not emit the event, if the browsing context is gone or not created yet. 976 if (context === null) { 977 // Save the realm info to send it when the browsing context is ready. 978 this.#realmInfoMap.set(realmInfo.context, realmInfo); 979 return; 980 } 981 this.#sendRealmCreatedEvent(realmInfo, realmInfo.context, context); 982 }; 983 984 #onRealmDestroyed = (eventName, { realm, context }) => { 985 this._emitEventForBrowsingContext(context.id, "script.realmDestroyed", { 986 realm, 987 }); 988 }; 989 990 #sendDelayedRealmCreatedEvent(browsingContext) { 991 const realmInfo = this.#realmInfoMap.get(browsingContext); 992 // Resolve browsing context to a TabManager id. 993 const browsingContextId = lazy.NavigableManager.getIdForBrowsingContext( 994 realmInfo.context 995 ); 996 this.#sendRealmCreatedEvent(realmInfo, browsingContext, browsingContextId); 997 this.#realmInfoMap.delete(browsingContext); 998 } 999 1000 #sendRealmCreatedEvent(realmInfo, context, browsingContextId) { 1001 realmInfo.context = browsingContextId; 1002 1003 this._emitEventForBrowsingContext( 1004 context.id, 1005 "script.realmCreated", 1006 realmInfo 1007 ); 1008 } 1009 1010 #startListeningOnRealmCreated() { 1011 if (!this.#subscribedEvents.has("script.realmCreated")) { 1012 this.messageHandler.on("realm-created", this.#onRealmCreated); 1013 this.messageHandler.on( 1014 "browsingContext._contextCreatedEmitted", 1015 this.#onContextCreatedSubmitted 1016 ); 1017 this.#contextListener.startListening(); 1018 } 1019 } 1020 1021 #stopListeningOnRealmCreated() { 1022 if (this.#subscribedEvents.has("script.realmCreated")) { 1023 this.messageHandler.off("realm-created", this.#onRealmCreated); 1024 this.messageHandler.off( 1025 "browsingContext._contextCreatedEmitted", 1026 this.#onContextCreatedSubmitted 1027 ); 1028 this.#contextListener.stopListening(); 1029 } 1030 } 1031 1032 #startListeningOnRealmDestroyed() { 1033 if (!this.#subscribedEvents.has("script.realmDestroyed")) { 1034 this.messageHandler.on("realm-destroyed", this.#onRealmDestroyed); 1035 } 1036 } 1037 1038 #stopListeningOnRealmDestroyed() { 1039 if (this.#subscribedEvents.has("script.realmDestroyed")) { 1040 this.messageHandler.off("realm-destroyed", this.#onRealmDestroyed); 1041 } 1042 } 1043 1044 #subscribeEvent(event) { 1045 switch (event) { 1046 case "script.realmCreated": { 1047 this.#startListeningOnRealmCreated(); 1048 this.#subscribedEvents.add(event); 1049 break; 1050 } 1051 case "script.realmDestroyed": { 1052 this.#startListeningOnRealmDestroyed(); 1053 this.#subscribedEvents.add(event); 1054 break; 1055 } 1056 } 1057 } 1058 1059 #unsubscribeEvent(event) { 1060 switch (event) { 1061 case "script.realmCreated": { 1062 this.#stopListeningOnRealmCreated(); 1063 this.#subscribedEvents.delete(event); 1064 break; 1065 } 1066 case "script.realmDestroyed": { 1067 this.#stopListeningOnRealmDestroyed(); 1068 this.#subscribedEvents.delete(event); 1069 break; 1070 } 1071 } 1072 } 1073 1074 _applySessionData(params) { 1075 // TODO: Bug 1775231. Move this logic to a shared module or an abstract 1076 // class. 1077 const { category } = params; 1078 if (category === "event") { 1079 const filteredSessionData = params.sessionData.filter(item => 1080 this.messageHandler.matchesContext(item.contextDescriptor) 1081 ); 1082 for (const event of this.#subscribedEvents.values()) { 1083 const hasSessionItem = filteredSessionData.some( 1084 item => item.value === event 1085 ); 1086 // If there are no session items for this context, we should unsubscribe from the event. 1087 if (!hasSessionItem) { 1088 this.#unsubscribeEvent(event); 1089 } 1090 } 1091 1092 // Subscribe to all events, which have an item in SessionData. 1093 for (const { value } of filteredSessionData) { 1094 this.#subscribeEvent(value); 1095 } 1096 } 1097 } 1098 1099 static get supportedEvents() { 1100 return ["script.message", "script.realmCreated", "script.realmDestroyed"]; 1101 } 1102 } 1103 1104 export const script = ScriptModule;