emulation.sys.mjs (36889B)
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 ContextDescriptorType: 12 "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs", 13 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 14 Log: "chrome://remote/content/shared/Log.sys.mjs", 15 NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs", 16 pprint: "chrome://remote/content/shared/Format.sys.mjs", 17 TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", 18 UserContextManager: 19 "chrome://remote/content/shared/UserContextManager.sys.mjs", 20 WindowGlobalMessageHandler: 21 "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs", 22 }); 23 24 ChromeUtils.defineLazyGetter(lazy, "logger", () => 25 lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI) 26 ); 27 28 /** 29 * Enum of possible natural orientations supported by the 30 * emulation.setOrientationOverride command. 31 * 32 * @readonly 33 * @enum {ScreenOrientationNatural} 34 */ 35 const ScreenOrientationNatural = { 36 Landscape: "landscape", 37 Portrait: "portrait", 38 }; 39 40 /** 41 * Enum of possible orientation types supported by the 42 * emulation.setOrientationOverride command. 43 * 44 * @readonly 45 * @enum {ScreenOrientationType} 46 */ 47 const ScreenOrientationType = { 48 PortraitPrimary: "portrait-primary", 49 PortraitSecondary: "portrait-secondary", 50 LandscapePrimary: "landscape-primary", 51 LandscapeSecondary: "landscape-secondary", 52 }; 53 54 // see https://www.w3.org/TR/screen-orientation/#dfn-screen-orientation-values-lists. 55 const SCREEN_ORIENTATION_VALUES_LISTS = { 56 [ScreenOrientationNatural.Portrait]: { 57 [ScreenOrientationType.PortraitPrimary]: 0, 58 [ScreenOrientationType.LandscapePrimary]: 90, 59 [ScreenOrientationType.PortraitSecondary]: 180, 60 [ScreenOrientationType.LandscapeSecondary]: 270, 61 }, 62 [ScreenOrientationNatural.Landscape]: { 63 [ScreenOrientationType.LandscapePrimary]: 0, 64 [ScreenOrientationType.PortraitPrimary]: 90, 65 [ScreenOrientationType.LandscapeSecondary]: 180, 66 [ScreenOrientationType.PortraitSecondary]: 270, 67 }, 68 }; 69 70 class EmulationModule extends RootBiDiModule { 71 /** 72 * Create a new module instance. 73 * 74 * @param {MessageHandler} messageHandler 75 * The MessageHandler instance which owns this Module instance. 76 */ 77 constructor(messageHandler) { 78 super(messageHandler); 79 } 80 81 destroy() {} 82 83 /** 84 * Used as an argument for emulation.setGeolocationOverride command 85 * to represent an object which holds geolocation coordinates which 86 * should override the return result of geolocation APIs. 87 * 88 * @typedef {object} GeolocationCoordinates 89 * 90 * @property {number} latitude 91 * @property {number} longitude 92 * @property {number=} accuracy 93 * Defaults to 1. 94 * @property {number=} altitude 95 * Defaults to null. 96 * @property {number=} altitudeAccuracy 97 * Defaults to null. 98 * @property {number=} heading 99 * Defaults to null. 100 * @property {number=} speed 101 * Defaults to null. 102 */ 103 104 /** 105 * Set the geolocation override to the list of top-level navigables 106 * or user contexts. 107 * 108 * @param {object=} options 109 * @param {Array<string>=} options.contexts 110 * Optional list of browsing context ids. 111 * @param {(GeolocationCoordinates|null)} options.coordinates 112 * Geolocation coordinates which have to override 113 * the return result of geolocation APIs. 114 * Null value resets the override. 115 * @param {Array<string>=} options.userContexts 116 * Optional list of user context ids. 117 * 118 * @throws {InvalidArgumentError} 119 * Raised if an argument is of an invalid type or value. 120 * @throws {NoSuchFrameError} 121 * If the browsing context cannot be found. 122 * @throws {NoSuchUserContextError} 123 * Raised if the user context id could not be found. 124 */ 125 async setGeolocationOverride(options = {}) { 126 let { coordinates } = options; 127 const { contexts: contextIds = null, userContexts: userContextIds = null } = 128 options; 129 130 if (coordinates !== null) { 131 lazy.assert.object( 132 coordinates, 133 lazy.pprint`Expected "coordinates" to be an object, got ${coordinates}` 134 ); 135 136 const { 137 latitude, 138 longitude, 139 accuracy = 1, 140 altitude = null, 141 altitudeAccuracy = null, 142 heading = null, 143 speed = null, 144 } = coordinates; 145 146 lazy.assert.numberInRange( 147 latitude, 148 [-90, 90], 149 lazy.pprint`Expected "latitude" to be in the range of -90 to 90, got ${latitude}` 150 ); 151 152 lazy.assert.numberInRange( 153 longitude, 154 [-180, 180], 155 lazy.pprint`Expected "longitude" to be in the range of -180 to 180, got ${longitude}` 156 ); 157 158 lazy.assert.positiveNumber( 159 accuracy, 160 lazy.pprint`Expected "accuracy" to be a positive number, got ${accuracy}` 161 ); 162 163 if (altitude !== null) { 164 lazy.assert.number( 165 altitude, 166 lazy.pprint`Expected "altitude" to be a number, got ${altitude}` 167 ); 168 } 169 170 if (altitudeAccuracy !== null) { 171 lazy.assert.positiveNumber( 172 altitudeAccuracy, 173 lazy.pprint`Expected "altitudeAccuracy" to be a positive number, got ${altitudeAccuracy}` 174 ); 175 176 if (altitude === null) { 177 throw new lazy.error.InvalidArgumentError( 178 `When "altitudeAccuracy" is provided it's required to provide "altitude" as well` 179 ); 180 } 181 } 182 183 if (heading !== null) { 184 lazy.assert.number( 185 heading, 186 lazy.pprint`Expected "heading" to be a number, got ${heading}` 187 ); 188 189 lazy.assert.that( 190 number => number >= 0 && number < 360, 191 lazy.pprint`Expected "heading" to be >= 0 and < 360, got ${heading}` 192 )(heading); 193 } 194 195 if (speed !== null) { 196 lazy.assert.positiveNumber( 197 speed, 198 lazy.pprint`Expected "speed" to be a positive number, got ${speed}` 199 ); 200 } 201 202 coordinates = { 203 ...coordinates, 204 accuracy, 205 // For platform API if we want to set values to null 206 // we have to set them to NaN. 207 altitude: altitude === null ? NaN : altitude, 208 altitudeAccuracy: altitudeAccuracy === null ? NaN : altitudeAccuracy, 209 heading: heading === null ? NaN : heading, 210 speed: speed === null ? NaN : speed, 211 }; 212 } 213 214 const { navigables, userContexts } = this.#getEmulationTargets( 215 contextIds, 216 userContextIds 217 ); 218 219 const sessionDataItems = this.#generateSessionDataUpdate({ 220 category: "geolocation-override", 221 contextOverride: contextIds !== null, 222 hasGlobalOverride: false, 223 navigables, 224 resetValue: null, 225 userContexts, 226 userContextOverride: userContextIds !== null, 227 value: coordinates, 228 }); 229 230 if (sessionDataItems.length) { 231 // TODO: Bug 1953079. Saving the geolocation override in the session data works fine 232 // with one session, but when we start supporting multiple BiDi session, we will 233 // have to rethink this approach. 234 await this.messageHandler.updateSessionData(sessionDataItems); 235 } 236 237 await this.#applyOverride({ 238 async: true, 239 callback: this.#applyGeolocationOverride.bind(this), 240 category: "geolocation-override", 241 contextIds, 242 navigables, 243 resetValue: null, 244 userContextIds, 245 value: coordinates, 246 }); 247 } 248 249 /** 250 * Set the locale override to the list of top-level navigables 251 * or user contexts. 252 * 253 * @param {object=} options 254 * @param {Array<string>=} options.contexts 255 * Optional list of browsing context ids. 256 * @param {(string|null)} options.locale 257 * Locale string which have to override 258 * the return result of JavaScript Intl APIs. 259 * Null value resets the override. 260 * @param {Array<string>=} options.userContexts 261 * Optional list of user context ids. 262 * 263 * @throws {InvalidArgumentError} 264 * Raised if an argument is of an invalid type or value. 265 * @throws {NoSuchFrameError} 266 * If the browsing context cannot be found. 267 * @throws {NoSuchUserContextError} 268 * Raised if the user context id could not be found. 269 */ 270 async setLocaleOverride(options = {}) { 271 const { 272 contexts: contextIds = null, 273 locale: localeArg, 274 userContexts: userContextIds = null, 275 } = options; 276 277 let locale; 278 if (localeArg === null) { 279 // The API requires an empty string to reset the override. 280 locale = ""; 281 } else { 282 locale = lazy.assert.string( 283 localeArg, 284 lazy.pprint`Expected "locale" to be a string, got ${localeArg}` 285 ); 286 287 // Validate if locale is a structurally valid language tag. 288 try { 289 Intl.getCanonicalLocales(localeArg); 290 } catch (err) { 291 if (err instanceof RangeError) { 292 throw new lazy.error.InvalidArgumentError( 293 `Expected "locale" to be a structurally valid language tag (e.g., "en-GB"), got ${localeArg}` 294 ); 295 } 296 297 throw err; 298 } 299 } 300 301 const { navigables, userContexts } = this.#getEmulationTargets( 302 contextIds, 303 userContextIds 304 ); 305 306 const sessionDataItems = this.#generateSessionDataUpdate({ 307 category: "locale-override", 308 contextOverride: contextIds !== null, 309 hasGlobalOverride: false, 310 navigables, 311 resetValue: "", 312 userContexts, 313 userContextOverride: userContextIds !== null, 314 value: locale, 315 }); 316 317 if (sessionDataItems.length) { 318 // TODO: Bug 1953079. Saving the locale override in the session data works fine 319 // with one session, but when we start supporting multiple BiDi session, we will 320 // have to rethink this approach. 321 await this.messageHandler.updateSessionData(sessionDataItems); 322 } 323 324 await this.#applyOverride({ 325 async: true, 326 callback: this._setLocaleForBrowsingContext.bind(this), 327 category: "locale-override", 328 contextIds, 329 navigables, 330 userContextIds, 331 value: locale, 332 }); 333 } 334 335 /** 336 * Used as an argument for emulation.setScreenOrientationOverride command 337 * to represent an object which holds screen orientation settings which 338 * should override screen settings. 339 * 340 * @typedef {object} ScreenOrientation 341 * 342 * @property {ScreenOrientationNatural} natural 343 * @property {ScreenOrientationType} type 344 */ 345 346 /** 347 * Set the screen orientation override to the list of 348 * top-level navigables or user contexts. 349 * 350 * @param {object=} options 351 * @param {Array<string>=} options.contexts 352 * Optional list of browsing context ids. 353 * @param {(ScreenOrientation|null)} options.screenOrientation 354 * Screen orientation object which have to override 355 * screen settings. 356 * Null value resets the override. 357 * @param {Array<string>=} options.userContexts 358 * Optional list of user context ids. 359 * 360 * @throws {InvalidArgumentError} 361 * Raised if an argument is of an invalid type or value. 362 * @throws {NoSuchFrameError} 363 * If the browsing context cannot be found. 364 * @throws {NoSuchUserContextError} 365 * Raised if the user context id could not be found. 366 */ 367 async setScreenOrientationOverride(options = {}) { 368 const { 369 contexts: contextIds = null, 370 screenOrientation, 371 userContexts: userContextIds = null, 372 } = options; 373 374 let orientationOverride; 375 376 if (screenOrientation !== null) { 377 lazy.assert.object( 378 screenOrientation, 379 lazy.pprint`Expected "screenOrientation" to be an object or null, got ${screenOrientation}` 380 ); 381 382 const { natural, type } = screenOrientation; 383 384 const naturalValues = Object.keys(SCREEN_ORIENTATION_VALUES_LISTS); 385 386 lazy.assert.in( 387 natural, 388 naturalValues, 389 `Expected "screenOrientation.natural" to be one of ${naturalValues},` + 390 lazy.pprint`got ${natural}` 391 ); 392 393 const orientationTypes = Object.keys( 394 SCREEN_ORIENTATION_VALUES_LISTS[natural] 395 ); 396 397 lazy.assert.in( 398 type, 399 orientationTypes, 400 lazy.pprint`Expected "screenOrientation.type" to be one of ${orientationTypes}` + 401 lazy.pprint`got ${type}` 402 ); 403 404 const angle = SCREEN_ORIENTATION_VALUES_LISTS[natural][type]; 405 406 orientationOverride = { angle, type }; 407 } else { 408 orientationOverride = null; 409 } 410 411 const { navigables, userContexts } = this.#getEmulationTargets( 412 contextIds, 413 userContextIds 414 ); 415 416 const sessionDataItems = this.#generateSessionDataUpdate({ 417 category: "screen-orientation-override", 418 contextOverride: contextIds !== null, 419 hasGlobalOverride: false, 420 navigables, 421 resetValue: null, 422 userContexts, 423 userContextOverride: userContextIds !== null, 424 value: orientationOverride, 425 }); 426 427 if (sessionDataItems.length) { 428 // TODO: Bug 1953079. Saving the screen orientation override in the session data works fine 429 // with one session, but when we start supporting multiple BiDi session, we will 430 // have to rethink this approach. 431 await this.messageHandler.updateSessionData(sessionDataItems); 432 } 433 434 this.#applyOverride({ 435 callback: this._setEmulatedScreenOrientation, 436 category: "screen-orientation-override", 437 contextIds, 438 navigables, 439 resetValue: null, 440 userContextIds, 441 value: orientationOverride, 442 }); 443 } 444 445 /** 446 * Used as an argument for emulation.setScreenSettingsOverride command 447 * to represent an object which holds screen area settings which 448 * should override screen dimensions. 449 * 450 * @typedef {object} ScreenArea 451 * 452 * @property {number} height 453 * @property {number} width 454 */ 455 456 /** 457 * Set the screen settings override to the list of top-level navigables 458 * or user contexts. 459 * 460 * @param {object=} options 461 * @param {Array<string>=} options.contexts 462 * Optional list of browsing context ids. 463 * @param {(ScreenArea|null)} options.screenArea 464 * An object which has to override 465 * the return result of JavaScript APIs which return 466 * screen dimensions. Null value resets the override. 467 * @param {Array<string>=} options.userContexts 468 * Optional list of user context ids. 469 * 470 * @throws {InvalidArgumentError} 471 * Raised if an argument is of an invalid type or value. 472 * @throws {NoSuchFrameError} 473 * If the browsing context cannot be found. 474 * @throws {NoSuchUserContextError} 475 * Raised if the user context id could not be found. 476 */ 477 async setScreenSettingsOverride(options = {}) { 478 const { 479 contexts: contextIds = null, 480 screenArea, 481 userContexts: userContextIds = null, 482 } = options; 483 484 if (screenArea !== null) { 485 lazy.assert.object( 486 screenArea, 487 lazy.pprint`Expected "screenArea" to be an object, got ${screenArea}` 488 ); 489 490 const { height, width } = screenArea; 491 lazy.assert.positiveNumber( 492 height, 493 lazy.pprint`Expected "screenArea.height" to be a positive number, got ${height}` 494 ); 495 lazy.assert.positiveNumber( 496 width, 497 lazy.pprint`Expected "screenArea.width" to be a positive number, got ${width}` 498 ); 499 } 500 501 const { navigables, userContexts } = this.#getEmulationTargets( 502 contextIds, 503 userContextIds 504 ); 505 506 const sessionDataItems = this.#generateSessionDataUpdate({ 507 category: "screen-settings-override", 508 contextOverride: contextIds !== null, 509 hasGlobalOverride: false, 510 navigables, 511 resetValue: null, 512 userContexts, 513 userContextOverride: userContextIds !== null, 514 value: screenArea, 515 }); 516 517 if (sessionDataItems.length) { 518 // TODO: Bug 1953079. Saving the locale override in the session data works fine 519 // with one session, but when we start supporting multiple BiDi session, we will 520 // have to rethink this approach. 521 await this.messageHandler.updateSessionData(sessionDataItems); 522 } 523 524 this.#applyOverride({ 525 callback: this._setScreenSettingsOverride, 526 category: "screen-settings-override", 527 contextIds, 528 navigables, 529 resetValue: null, 530 userContextIds, 531 value: screenArea, 532 }); 533 } 534 535 /** 536 * Set the timezone override to the list of top-level navigables 537 * or user contexts. 538 * 539 * @param {object=} options 540 * @param {Array<string>=} options.contexts 541 * Optional list of browsing context ids. 542 * @param {(string|null)} options.timezone 543 * Timezone string which has to override 544 * the return result of JavaScript Intl/Date APIs. 545 * It can represent timezone id or timezone offset. 546 * Null value resets the override. 547 * @param {Array<string>=} options.userContexts 548 * Optional list of user context ids. 549 * 550 * @throws {InvalidArgumentError} 551 * Raised if an argument is of an invalid type or value. 552 * @throws {NoSuchFrameError} 553 * If the browsing context cannot be found. 554 * @throws {NoSuchUserContextError} 555 * Raised if the user context id could not be found. 556 */ 557 async setTimezoneOverride(options = {}) { 558 let { timezone } = options; 559 const { contexts: contextIds = null, userContexts: userContextIds = null } = 560 options; 561 562 if (timezone === null) { 563 // The API requires an empty string to reset the override. 564 timezone = ""; 565 } else { 566 lazy.assert.string( 567 timezone, 568 lazy.pprint`Expected "timezone" to be a string, got ${timezone}` 569 ); 570 571 if ( 572 // Validate if the timezone is on the list of available timezones ids 573 !Intl.supportedValuesOf("timeZone").includes(timezone) && 574 // or is a valid timezone offset string. 575 !this.#isTimeZoneOffsetString(timezone) 576 ) { 577 throw new lazy.error.InvalidArgumentError( 578 `Expected "timezone" to be a valid timezone ID (e.g., "Europe/Berlin") ` + 579 `or a valid timezone offset (e.g., "+01:00"), got ${timezone}` 580 ); 581 } 582 583 if (this.#isTimeZoneOffsetString(timezone)) { 584 // The platform API requires a timezone offset to have a "GMT" prefix. 585 timezone = `GMT${timezone}`; 586 } 587 } 588 589 const { navigables, userContexts } = this.#getEmulationTargets( 590 contextIds, 591 userContextIds 592 ); 593 594 const sessionDataItems = this.#generateSessionDataUpdate({ 595 category: "timezone-override", 596 contextOverride: contextIds !== null, 597 hasGlobalOverride: false, 598 navigables, 599 resetValue: "", 600 userContexts, 601 userContextOverride: userContextIds !== null, 602 value: timezone, 603 }); 604 605 if (sessionDataItems.length) { 606 // TODO: Bug 1953079. Saving the timezone override in the session data works fine 607 // with one session, but when we start supporting multiple BiDi session, we will 608 // have to rethink this approach. 609 await this.messageHandler.updateSessionData(sessionDataItems); 610 } 611 612 await this.#applyOverride({ 613 async: true, 614 callback: this._setTimezoneOverride.bind(this), 615 category: "timezone-override", 616 contextIds, 617 navigables, 618 userContextIds, 619 value: timezone, 620 }); 621 } 622 623 /** 624 * Set the user agent override to the list of top-level navigables 625 * or user contexts. 626 * 627 * @param {object=} options 628 * @param {Array<string>=} options.contexts 629 * Optional list of browsing context ids. 630 * @param {(string|null)} options.userAgent 631 * User agent string which has to override 632 * the browser user agent. 633 * Null value resets the override. 634 * @param {Array<string>=} options.userContexts 635 * Optional list of user context ids. 636 * 637 * @throws {InvalidArgumentError} 638 * Raised if an argument is of an invalid type or value. 639 * @throws {NoSuchFrameError} 640 * If the browsing context cannot be found. 641 * @throws {NoSuchUserContextError} 642 * Raised if the user context id could not be found. 643 */ 644 async setUserAgentOverride(options = {}) { 645 const { contexts: contextIds, userContexts: userContextIds } = options; 646 let { userAgent } = options; 647 648 if (userAgent === null) { 649 // The API requires an empty string to reset the override. 650 userAgent = ""; 651 } else { 652 lazy.assert.string( 653 userAgent, 654 lazy.pprint`Expected "userAgent" to be a string, got ${userAgent}` 655 ); 656 657 if (userAgent === "") { 658 throw new lazy.error.UnsupportedOperationError( 659 `Overriding "userAgent" to an empty string is not supported` 660 ); 661 } 662 } 663 664 if (contextIds !== undefined && userContextIds !== undefined) { 665 throw new lazy.error.InvalidArgumentError( 666 `Providing both "contexts" and "userContexts" arguments is not supported` 667 ); 668 } 669 670 const navigables = new Set(); 671 const userContexts = new Set(); 672 if (contextIds !== undefined) { 673 lazy.assert.isNonEmptyArray( 674 contextIds, 675 lazy.pprint`Expected "contexts" to be a non-empty array, got ${contextIds}` 676 ); 677 678 for (const contextId of contextIds) { 679 lazy.assert.string( 680 contextId, 681 lazy.pprint`Expected elements of "contexts" to be a string, got ${contextId}` 682 ); 683 684 const context = this._getNavigable(contextId); 685 686 lazy.assert.topLevel( 687 context, 688 `Browsing context with id ${contextId} is not top-level` 689 ); 690 691 navigables.add(context); 692 } 693 } else if (userContextIds !== undefined) { 694 lazy.assert.isNonEmptyArray( 695 userContextIds, 696 lazy.pprint`Expected "userContexts" to be a non-empty array, got ${userContextIds}` 697 ); 698 699 for (const userContextId of userContextIds) { 700 lazy.assert.string( 701 userContextId, 702 lazy.pprint`Expected elements of "userContexts" to be a string, got ${userContextId}` 703 ); 704 705 const internalId = 706 lazy.UserContextManager.getInternalIdById(userContextId); 707 708 if (internalId === null) { 709 throw new lazy.error.NoSuchUserContextError( 710 `User context with id: ${userContextId} doesn't exist` 711 ); 712 } 713 714 userContexts.add(internalId); 715 716 // Prepare the list of navigables to update. 717 lazy.UserContextManager.getTabsForUserContext(internalId).forEach( 718 tab => { 719 const contentBrowser = lazy.TabManager.getBrowserForTab(tab); 720 navigables.add(contentBrowser.browsingContext); 721 } 722 ); 723 } 724 } else { 725 lazy.TabManager.getBrowsers().forEach(browser => 726 navigables.add(browser.browsingContext) 727 ); 728 } 729 730 const sessionDataItems = this.#generateSessionDataUpdate({ 731 category: "user-agent-override", 732 contextOverride: contextIds !== undefined, 733 hasGlobalOverride: true, 734 navigables, 735 resetValue: "", 736 userContexts, 737 userContextOverride: userContextIds !== undefined, 738 value: userAgent, 739 }); 740 741 if (sessionDataItems.length) { 742 // TODO: Bug 1953079. Saving the user agent override in the session data works fine 743 // with one session, but when we start supporting multiple BiDi session, we will 744 // have to rethink this approach. 745 await this.messageHandler.updateSessionData(sessionDataItems); 746 } 747 748 this.#applyOverride({ 749 callback: this._setUserAgentOverride, 750 category: "user-agent-override", 751 contextIds, 752 navigables, 753 userContextIds, 754 value: userAgent, 755 }); 756 } 757 758 /** 759 * Set the screen orientation override to the top-level browsing context. 760 * 761 * @param {object} options 762 * @param {BrowsingContext} options.context 763 * Top-level browsing context object which is a target 764 * for the screen orientation override. 765 * @param {(object|null)} options.value 766 * Screen orientation object which have to override 767 * screen settings. 768 * Null value resets the override. 769 */ 770 _setEmulatedScreenOrientation(options) { 771 const { context, value } = options; 772 if (value) { 773 const { angle, type } = value; 774 context.setOrientationOverride(type, angle); 775 } else { 776 context.resetOrientationOverride(); 777 } 778 } 779 780 /** 781 * Set the locale override to the top-level browsing context. 782 * 783 * @param {object} options 784 * @param {BrowsingContext} options.context 785 * Top-level browsing context object which is a target 786 * for the locale override. 787 * @param {(string|null)} options.value 788 * Locale string which have to override 789 * the return result of JavaScript Intl APIs. 790 * Null value resets the override. 791 */ 792 async _setLocaleForBrowsingContext(options) { 793 const { context, value } = options; 794 795 context.languageOverride = value; 796 797 await this.messageHandler.handleCommand({ 798 moduleName: "emulation", 799 commandName: "_setLocaleOverrideToSandboxes", 800 destination: { 801 type: lazy.WindowGlobalMessageHandler.type, 802 contextDescriptor: { 803 type: lazy.ContextDescriptorType.TopBrowsingContext, 804 id: context.browserId, 805 }, 806 }, 807 params: { 808 locale: value, 809 }, 810 }); 811 } 812 813 /** 814 * Set the screen settings override to the top-level browsing context. 815 * 816 * @param {object} options 817 * @param {BrowsingContext} options.context 818 * Top-level browsing context object which is a target 819 * for the locale override. 820 * @param {(ScreenArea|null)} options.value 821 * An object which has to override 822 * the return result of JavaScript APIs which return 823 * screen dimensions. Null value resets the override. 824 */ 825 _setScreenSettingsOverride(options) { 826 const { context, value } = options; 827 828 if (value === null) { 829 context.resetScreenAreaOverride(); 830 } else { 831 const { height, width } = value; 832 context.setScreenAreaOverride(width, height); 833 } 834 } 835 836 /** 837 * Set the timezone override to the top-level browsing context. 838 * 839 * @param {object} options 840 * @param {BrowsingContext} options.context 841 * Top-level browsing context object which is a target 842 * for the locale override. 843 * @param {(string|null)} options.value 844 * Timezone string which has to override 845 * the return result of JavaScript Intl/Date APIs. 846 * Null value resets the override. 847 */ 848 async _setTimezoneOverride(options) { 849 const { context, value } = options; 850 851 context.timezoneOverride = value; 852 853 await this.messageHandler.handleCommand({ 854 moduleName: "emulation", 855 commandName: "_setTimezoneOverrideToSandboxes", 856 destination: { 857 type: lazy.WindowGlobalMessageHandler.type, 858 contextDescriptor: { 859 type: lazy.ContextDescriptorType.TopBrowsingContext, 860 id: context.browserId, 861 }, 862 }, 863 params: { 864 timezone: value, 865 }, 866 }); 867 } 868 869 /** 870 * Set the user agent override to the top-level browsing context. 871 * 872 * @param {object} options 873 * @param {BrowsingContext} options.context 874 * Top-level browsing context object which is a target 875 * for the locale override. 876 * @param {string} options.value 877 * User agent string which has to override 878 * the browser user agent. 879 */ 880 _setUserAgentOverride(options) { 881 const { context, value } = options; 882 883 try { 884 context.customUserAgent = value; 885 } catch (e) { 886 const contextId = lazy.NavigableManager.getIdForBrowsingContext(context); 887 888 lazy.logger.warn( 889 `Failed to override user agent for context with id: ${contextId} (${e.message})` 890 ); 891 } 892 } 893 894 /** 895 * Apply the geolocation override to the top-level browsing context. 896 * 897 * @param {object} options 898 * @param {BrowsingContext} options.context 899 * Top-level browsing context object which is a target 900 * for the geolocation override. 901 * @param {(GeolocationCoordinates|null)} options.value 902 * Geolocation coordinates which have to override 903 * the return result of geolocation APIs. 904 * Null value resets the override. 905 */ 906 #applyGeolocationOverride(options) { 907 const { context, value } = options; 908 909 return this._forwardToWindowGlobal( 910 "_setGeolocationOverride", 911 context.id, 912 { 913 coordinates: value, 914 }, 915 { retryOnAbort: true } 916 ); 917 } 918 919 async #applyOverride(options) { 920 const { 921 async = false, 922 callback, 923 category, 924 contextIds, 925 navigables, 926 resetValue = "", 927 userContextIds, 928 value, 929 } = options; 930 931 const commands = []; 932 933 for (const navigable of navigables) { 934 const overrideValue = this.#getOverrideValue( 935 { 936 category, 937 context: navigable, 938 contextIds, 939 userContextIds, 940 value, 941 }, 942 resetValue 943 ); 944 945 if (overrideValue === undefined) { 946 continue; 947 } 948 949 const commandArgs = { 950 context: navigable, 951 value: overrideValue, 952 }; 953 954 if (async) { 955 commands.push(callback(commandArgs)); 956 } else { 957 callback(commandArgs); 958 } 959 } 960 961 if (async) { 962 await Promise.all(commands); 963 } 964 } 965 966 #generateSessionDataUpdate(options) { 967 const { 968 category, 969 contextOverride, 970 hasGlobalOverride, 971 navigables, 972 resetValue, 973 userContexts, 974 userContextOverride, 975 value, 976 } = options; 977 const sessionDataItems = []; 978 const onlyRemoveSessionDataItem = value === resetValue; 979 980 if (userContextOverride) { 981 for (const userContext of userContexts) { 982 sessionDataItems.push( 983 ...this.messageHandler.sessionData.generateSessionDataItemUpdate( 984 "_configuration", 985 category, 986 { 987 type: lazy.ContextDescriptorType.UserContext, 988 id: userContext, 989 }, 990 onlyRemoveSessionDataItem, 991 value 992 ) 993 ); 994 } 995 } else if (contextOverride) { 996 for (const navigable of navigables) { 997 sessionDataItems.push( 998 ...this.messageHandler.sessionData.generateSessionDataItemUpdate( 999 "_configuration", 1000 category, 1001 { 1002 type: lazy.ContextDescriptorType.TopBrowsingContext, 1003 id: navigable.browserId, 1004 }, 1005 onlyRemoveSessionDataItem, 1006 value 1007 ) 1008 ); 1009 } 1010 } else if (hasGlobalOverride) { 1011 sessionDataItems.push( 1012 ...this.messageHandler.sessionData.generateSessionDataItemUpdate( 1013 "_configuration", 1014 category, 1015 { 1016 type: lazy.ContextDescriptorType.All, 1017 }, 1018 onlyRemoveSessionDataItem, 1019 value 1020 ) 1021 ); 1022 } 1023 1024 return sessionDataItems; 1025 } 1026 1027 /** 1028 * Return value for #getEmulationTargets. 1029 * 1030 * @typedef {object} EmulationTargets 1031 * 1032 * @property {Set<Navigable>} navigables 1033 * @property {Set<number>} userContexts 1034 */ 1035 1036 /** 1037 * Validates the provided browsing contexts or user contexts and resolves them 1038 * to a set of navigables. 1039 * 1040 * @param {Array<string>|null} contextIds 1041 * Optional list of browsing context ids. 1042 * @param {Array<string>|null} userContextIds 1043 * Optional list of user context ids. 1044 * 1045 * @returns {EmulationTargets} 1046 */ 1047 #getEmulationTargets(contextIds, userContextIds) { 1048 if (contextIds !== null && userContextIds !== null) { 1049 throw new lazy.error.InvalidArgumentError( 1050 `Providing both "contexts" and "userContexts" arguments is not supported` 1051 ); 1052 } 1053 1054 const navigables = new Set(); 1055 const userContexts = new Set(); 1056 1057 if (contextIds !== null) { 1058 lazy.assert.isNonEmptyArray( 1059 contextIds, 1060 lazy.pprint`Expected "contexts" to be a non-empty array, got ${contextIds}` 1061 ); 1062 1063 for (const contextId of contextIds) { 1064 lazy.assert.string( 1065 contextId, 1066 lazy.pprint`Expected elements of "contexts" to be a string, got ${contextId}` 1067 ); 1068 1069 const context = this._getNavigable(contextId); 1070 1071 lazy.assert.topLevel( 1072 context, 1073 `Browsing context with id ${contextId} is not top-level` 1074 ); 1075 1076 navigables.add(context); 1077 } 1078 } else if (userContextIds !== null) { 1079 lazy.assert.isNonEmptyArray( 1080 userContextIds, 1081 lazy.pprint`Expected "userContexts" to be a non-empty array, got ${userContextIds}` 1082 ); 1083 1084 for (const userContextId of userContextIds) { 1085 lazy.assert.string( 1086 userContextId, 1087 lazy.pprint`Expected elements of "userContexts" to be a string, got ${userContextId}` 1088 ); 1089 1090 const internalId = 1091 lazy.UserContextManager.getInternalIdById(userContextId); 1092 1093 if (internalId === null) { 1094 throw new lazy.error.NoSuchUserContextError( 1095 `User context with id: ${userContextId} doesn't exist` 1096 ); 1097 } 1098 1099 userContexts.add(internalId); 1100 1101 // Prepare the list of navigables to update. 1102 lazy.UserContextManager.getTabsForUserContext(internalId).forEach( 1103 tab => { 1104 const contentBrowser = lazy.TabManager.getBrowserForTab(tab); 1105 navigables.add(contentBrowser.browsingContext); 1106 } 1107 ); 1108 } 1109 } else { 1110 throw new lazy.error.InvalidArgumentError( 1111 `At least one of "contexts" or "userContexts" arguments should be provided` 1112 ); 1113 } 1114 1115 return { navigables, userContexts }; 1116 } 1117 1118 #getOverrideValue(params, resetValue = "") { 1119 const { category, context, contextIds, userContextIds, value } = params; 1120 const [overridePerContext, overridePerUserContext, overrideGlobal] = 1121 this.#findExistingOverrideForContext(category, context); 1122 1123 if (contextIds) { 1124 if (value === resetValue) { 1125 // In case of resetting an override for navigable, 1126 // if there is an existing override for user context or global, 1127 // we should apply it to browsing context. 1128 return overridePerUserContext || overrideGlobal || resetValue; 1129 } 1130 } else if (userContextIds) { 1131 // No need to do anything if there is an override 1132 // for the browsing context. 1133 if (overridePerContext) { 1134 return undefined; 1135 } 1136 1137 // In case of resetting an override for user context, 1138 // apply a global override if it exists 1139 if (value === resetValue && overrideGlobal) { 1140 return overrideGlobal; 1141 } 1142 } else if (overridePerContext || overridePerUserContext) { 1143 // No need to do anything if there is an override 1144 // for the browsing or user context. 1145 return undefined; 1146 } 1147 1148 return value; 1149 } 1150 1151 /** 1152 * Find the existing overrides for a given category and context. 1153 * 1154 * @param {string} category 1155 * The session data category. 1156 * @param {BrowsingContext} context 1157 * The browsing context. 1158 * 1159 * @returns {Array<string>} 1160 * Return the list of existing values. 1161 */ 1162 #findExistingOverrideForContext(category, context) { 1163 let overrideGlobal, overridePerUserContext, overridePerContext; 1164 1165 const sessionDataItems = 1166 this.messageHandler.sessionData.getSessionDataForContext( 1167 "_configuration", 1168 category, 1169 context 1170 ); 1171 1172 sessionDataItems.forEach(item => { 1173 switch (item.contextDescriptor.type) { 1174 case lazy.ContextDescriptorType.All: { 1175 overrideGlobal = item.value; 1176 break; 1177 } 1178 case lazy.ContextDescriptorType.UserContext: { 1179 overridePerUserContext = item.value; 1180 break; 1181 } 1182 case lazy.ContextDescriptorType.TopBrowsingContext: { 1183 overridePerContext = item.value; 1184 break; 1185 } 1186 } 1187 }); 1188 1189 return [overridePerContext, overridePerUserContext, overrideGlobal]; 1190 } 1191 1192 /** 1193 * Validate that a string has timezone offset string format 1194 * (e.g. `+10:00` or `-05:00`). 1195 * 1196 * @see https://tc39.es/ecma262/#sec-time-zone-offset-strings. 1197 * 1198 * @param {string} string 1199 * The string to validate. 1200 * 1201 * @returns {boolean} 1202 * Return true if the string has timezone offset string format, 1203 * false otherwise. 1204 */ 1205 #isTimeZoneOffsetString(string) { 1206 if (string === "" || string === "Z") { 1207 return false; 1208 } 1209 // Random date string is added to validate an offset string. 1210 return ChromeUtils.isISOStyleDate(`2011-10-05T00:00${string}`); 1211 } 1212 } 1213 1214 export const emulation = EmulationModule;