Capabilities.sys.mjs (30320B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs", 11 assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", 12 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 13 pprint: "chrome://remote/content/shared/Format.sys.mjs", 14 truncate: "chrome://remote/content/shared/Format.sys.mjs", 15 UserPromptHandler: 16 "chrome://remote/content/shared/webdriver/UserPromptHandler.sys.mjs", 17 }); 18 19 ChromeUtils.defineLazyGetter(lazy, "isHeadless", () => { 20 return Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless; 21 }); 22 23 ChromeUtils.defineLazyGetter(lazy, "userAgent", () => { 24 return Cc["@mozilla.org/network/protocol;1?name=http"].getService( 25 Ci.nsIHttpProtocolHandler 26 ).userAgent; 27 }); 28 29 XPCOMUtils.defineLazyPreferenceGetter( 30 lazy, 31 "shutdownTimeout", 32 "toolkit.asyncshutdown.crash_timeout" 33 ); 34 35 // List of capabilities which are only relevant for Webdriver Classic. 36 export const WEBDRIVER_CLASSIC_CAPABILITIES = [ 37 "pageLoadStrategy", 38 "strictFileInteractability", 39 "timeouts", 40 "webSocketUrl", 41 42 // Gecko specific capabilities 43 "moz:accessibilityChecks", 44 "moz:firefoxOptions", 45 "moz:webdriverClick", 46 47 // Extension capabilities 48 "webauthn:extension:credBlob", 49 "webauthn:extension:largeBlob", 50 "webauthn:extension:prf", 51 "webauthn:extension:uvm", 52 "webauthn:virtualAuthenticators", 53 ]; 54 55 /** Representation of WebDriver session timeouts. */ 56 export class Timeouts { 57 constructor() { 58 // disabled 59 this.implicit = 0; 60 // five minutes 61 this.pageLoad = 300000; 62 // 30 seconds 63 this.script = 30000; 64 } 65 66 toString() { 67 return "[object Timeouts]"; 68 } 69 70 /** Marshals timeout durations to a JSON Object. */ 71 toJSON() { 72 return { 73 implicit: this.implicit, 74 pageLoad: this.pageLoad, 75 script: this.script, 76 }; 77 } 78 79 static fromJSON(json) { 80 lazy.assert.object( 81 json, 82 lazy.pprint`Expected "timeouts" to be an object, got ${json}` 83 ); 84 let t = new Timeouts(); 85 86 for (let [type, ms] of Object.entries(json)) { 87 switch (type) { 88 case "implicit": 89 t.implicit = lazy.assert.positiveInteger( 90 ms, 91 `Expected "${type}" to be a positive integer, ` + 92 lazy.pprint`got ${ms}` 93 ); 94 break; 95 96 case "script": 97 if (ms !== null) { 98 lazy.assert.positiveInteger( 99 ms, 100 `Expected "${type}" to be a positive integer, ` + 101 lazy.pprint`got ${ms}` 102 ); 103 } 104 t.script = ms; 105 break; 106 107 case "pageLoad": 108 t.pageLoad = lazy.assert.positiveInteger( 109 ms, 110 `Expected "${type}" to be a positive integer, ` + 111 lazy.pprint`got ${ms}` 112 ); 113 break; 114 115 default: 116 throw new lazy.error.InvalidArgumentError( 117 `Unrecognized timeout: ${type}` 118 ); 119 } 120 } 121 122 return t; 123 } 124 } 125 126 /** 127 * Enum of page loading strategies. 128 * 129 * @enum 130 */ 131 export const PageLoadStrategy = { 132 /** No page load strategy. Navigation will return immediately. */ 133 None: "none", 134 /** 135 * Eager, causing navigation to complete when the document reaches 136 * the <code>interactive</code> ready state. 137 */ 138 Eager: "eager", 139 /** 140 * Normal, causing navigation to return when the document reaches the 141 * <code>complete</code> ready state. 142 */ 143 Normal: "normal", 144 }; 145 146 /** 147 * Enum of proxy types. 148 * 149 * @enum 150 */ 151 export const ProxyTypes = { 152 Autodetect: "autodetect", 153 Direct: "direct", 154 Manual: "manual", 155 Pac: "pac", 156 System: "system", 157 }; 158 159 /** Proxy configuration object representation. */ 160 export class ProxyConfiguration { 161 #previousValuesForPreferences; 162 163 /** @class */ 164 constructor() { 165 this.proxyType = null; 166 this.httpProxy = null; 167 this.httpProxyPort = null; 168 this.noProxy = null; 169 this.sslProxy = null; 170 this.sslProxyPort = null; 171 this.socksProxy = null; 172 this.socksProxyPort = null; 173 this.socksVersion = null; 174 this.proxyAutoconfigUrl = null; 175 176 // List of applied preferences to clean up on destroy. 177 this.#previousValuesForPreferences = new Set(); 178 } 179 180 destroy() { 181 for (const { type, name, value } of this.#previousValuesForPreferences) { 182 if (type === "int") { 183 Services.prefs.setIntPref(name, value); 184 } else if (type === "string") { 185 Services.prefs.setStringPref(name, value); 186 } 187 } 188 189 this.#previousValuesForPreferences = new Set(); 190 } 191 192 /** 193 * Sets Firefox proxy settings. 194 * 195 * @returns {boolean} 196 * True if proxy settings were updated as a result of calling this 197 * function, or false indicating that this function acted as 198 * a no-op. 199 */ 200 init() { 201 switch (this.proxyType) { 202 case ProxyTypes.Autodetect: 203 this.#setPreference("network.proxy.type", 4); 204 return true; 205 206 case ProxyTypes.Direct: 207 this.#setPreference("network.proxy.type", 0); 208 return true; 209 210 case ProxyTypes.Manual: 211 this.#setPreference("network.proxy.type", 1); 212 213 if (this.httpProxy) { 214 this.#setPreference("network.proxy.http", this.httpProxy, "string"); 215 if (Number.isInteger(this.httpProxyPort)) { 216 this.#setPreference("network.proxy.http_port", this.httpProxyPort); 217 } 218 } 219 220 if (this.sslProxy) { 221 this.#setPreference("network.proxy.ssl", this.sslProxy, "string"); 222 if (Number.isInteger(this.sslProxyPort)) { 223 this.#setPreference("network.proxy.ssl_port", this.sslProxyPort); 224 } 225 } 226 227 if (this.socksProxy) { 228 this.#setPreference("network.proxy.socks", this.socksProxy, "string"); 229 if (Number.isInteger(this.socksProxyPort)) { 230 this.#setPreference( 231 "network.proxy.socks_port", 232 this.socksProxyPort 233 ); 234 } 235 if (this.socksVersion) { 236 this.#setPreference( 237 "network.proxy.socks_version", 238 this.socksVersion 239 ); 240 } 241 } 242 243 if (this.noProxy) { 244 this.#setPreference( 245 "network.proxy.no_proxies_on", 246 this.noProxy.join(", "), 247 "string" 248 ); 249 } 250 return true; 251 252 case ProxyTypes.Pac: 253 this.#setPreference("network.proxy.type", 2); 254 this.#setPreference( 255 "network.proxy.autoconfig_url", 256 this.proxyAutoconfigUrl, 257 "string" 258 ); 259 return true; 260 261 case ProxyTypes.System: 262 this.#setPreference("network.proxy.type", 5); 263 return true; 264 265 default: 266 return false; 267 } 268 } 269 270 /** 271 * @param {Record<string, ?>} json 272 * JSON Object to unmarshal. 273 * 274 * @throws {InvalidArgumentError} 275 * When proxy configuration is invalid. 276 */ 277 static fromJSON(json) { 278 function stripBracketsFromIpv6Hostname(hostname) { 279 return hostname.includes(":") 280 ? hostname.replace(/[\[\]]/g, "") 281 : hostname; 282 } 283 284 // Parse hostname and optional port from host 285 function fromHost(scheme, host) { 286 lazy.assert.string( 287 host, 288 lazy.pprint`Expected proxy "host" to be a string, got ${host}` 289 ); 290 291 if (host.includes("://")) { 292 throw new lazy.error.InvalidArgumentError(`${host} contains a scheme`); 293 } 294 295 // To parse the host a scheme has to be added temporarily. 296 // If the returned value for the port is an empty string it 297 // could mean no port or the default port for this scheme was 298 // specified. In such a case parse again with a different 299 // scheme to ensure we filter out the default port. 300 let url; 301 for (let _url of [`http://${host}`, `https://${host}`]) { 302 url = URL.parse(_url); 303 if (!url) { 304 throw new lazy.error.InvalidArgumentError( 305 lazy.truncate`Expected "url" to be a valid URL, got ${_url}` 306 ); 307 } 308 if (url.port != "") { 309 break; 310 } 311 } 312 313 if ( 314 url.username != "" || 315 url.password != "" || 316 url.pathname != "/" || 317 url.search != "" || 318 url.hash != "" 319 ) { 320 throw new lazy.error.InvalidArgumentError( 321 `${host} was not of the form host[:port]` 322 ); 323 } 324 325 let hostname = stripBracketsFromIpv6Hostname(url.hostname); 326 327 // If the port hasn't been set, use the default port of 328 // the selected scheme (except for socks which doesn't have one). 329 let port = parseInt(url.port); 330 if (!Number.isInteger(port)) { 331 if (scheme === "socks") { 332 port = null; 333 } else { 334 port = Services.io.getDefaultPort(scheme); 335 } 336 } 337 338 return [hostname, port]; 339 } 340 341 let p = new ProxyConfiguration(); 342 if (typeof json == "undefined" || json === null) { 343 return p; 344 } 345 346 lazy.assert.object( 347 json, 348 lazy.pprint`Expected "proxy" to be an object, got ${json}` 349 ); 350 351 lazy.assert.in( 352 "proxyType", 353 json, 354 lazy.pprint`Expected "proxyType" in "proxy" object, got ${json}` 355 ); 356 p.proxyType = lazy.assert.string( 357 json.proxyType, 358 lazy.pprint`Expected "proxyType" to be a string, got ${json.proxyType}` 359 ); 360 361 switch (p.proxyType) { 362 case "autodetect": 363 case "direct": 364 case "system": 365 break; 366 367 case "pac": 368 p.proxyAutoconfigUrl = lazy.assert.string( 369 json.proxyAutoconfigUrl, 370 `Expected "proxyAutoconfigUrl" to be a string, ` + 371 lazy.pprint`got ${json.proxyAutoconfigUrl}` 372 ); 373 break; 374 375 case "manual": 376 if (typeof json.httpProxy != "undefined") { 377 [p.httpProxy, p.httpProxyPort] = fromHost("http", json.httpProxy); 378 } 379 if (typeof json.sslProxy != "undefined") { 380 [p.sslProxy, p.sslProxyPort] = fromHost("https", json.sslProxy); 381 } 382 if (typeof json.socksProxy != "undefined") { 383 [p.socksProxy, p.socksProxyPort] = fromHost("socks", json.socksProxy); 384 p.socksVersion = lazy.assert.positiveInteger( 385 json.socksVersion, 386 lazy.pprint`Expected "socksVersion" to be a positive integer, got ${json.socksVersion}` 387 ); 388 } 389 if ( 390 typeof json.socksVersion != "undefined" && 391 typeof json.socksProxy == "undefined" 392 ) { 393 throw new lazy.error.InvalidArgumentError( 394 `Expected "socksProxy" to be provided if "socksVersion" is provided, got ${json.socksProxy}` 395 ); 396 } 397 if (typeof json.noProxy != "undefined") { 398 let entries = lazy.assert.array( 399 json.noProxy, 400 lazy.pprint`Expected "noProxy" to be an array, got ${json.noProxy}` 401 ); 402 p.noProxy = entries.map(entry => { 403 lazy.assert.string( 404 entry, 405 lazy.pprint`Expected "noProxy" entry to be a string, got ${entry}` 406 ); 407 return stripBracketsFromIpv6Hostname(entry); 408 }); 409 } 410 break; 411 412 default: 413 throw new lazy.error.InvalidArgumentError( 414 `Invalid type of proxy: ${p.proxyType}` 415 ); 416 } 417 418 return p; 419 } 420 421 /** 422 * @returns {Record<string, (number | string)>} 423 * JSON serialisation of proxy object. 424 */ 425 toJSON() { 426 function addBracketsToIpv6Hostname(hostname) { 427 return hostname.includes(":") ? `[${hostname}]` : hostname; 428 } 429 430 function toHost(hostname, port) { 431 if (!hostname) { 432 return null; 433 } 434 435 // Add brackets around IPv6 addresses 436 hostname = addBracketsToIpv6Hostname(hostname); 437 438 if (port != null) { 439 return `${hostname}:${port}`; 440 } 441 442 return hostname; 443 } 444 445 let excludes = this.noProxy; 446 if (excludes) { 447 excludes = excludes.map(addBracketsToIpv6Hostname); 448 } 449 450 return marshal({ 451 proxyType: this.proxyType, 452 httpProxy: toHost(this.httpProxy, this.httpProxyPort), 453 noProxy: excludes, 454 sslProxy: toHost(this.sslProxy, this.sslProxyPort), 455 socksProxy: toHost(this.socksProxy, this.socksProxyPort), 456 socksVersion: this.socksVersion, 457 proxyAutoconfigUrl: this.proxyAutoconfigUrl, 458 }); 459 } 460 461 toString() { 462 return "[object Proxy]"; 463 } 464 465 #setPreference(name, value, type = "int") { 466 let prevValue; 467 468 if (type === "int") { 469 if (Services.prefs.getPrefType(name) != Services.prefs.PREF_INVALID) { 470 prevValue = Services.prefs.getIntPref(name); 471 } 472 473 Services.prefs.setIntPref(name, value); 474 } else if (type === "string") { 475 if (Services.prefs.getPrefType(name) != Services.prefs.PREF_INVALID) { 476 prevValue = Services.prefs.getStringPref(name); 477 } 478 479 Services.prefs.setStringPref(name, value); 480 } 481 482 if (prevValue !== undefined) { 483 this.#previousValuesForPreferences.add({ name, type, value: prevValue }); 484 } 485 } 486 } 487 488 export class Capabilities extends Map { 489 /** 490 * WebDriver session capabilities representation. 491 * 492 * @param {boolean} isBidi 493 * Flag indicating that it is a WebDriver BiDi session. Defaults to false. 494 */ 495 constructor(isBidi = false) { 496 // Default values for capabilities supported by both WebDriver protocols 497 const defaults = [ 498 ["acceptInsecureCerts", false], 499 ["browserName", getWebDriverBrowserName()], 500 ["browserVersion", lazy.AppInfo.version], 501 ["platformName", getWebDriverPlatformName()], 502 ["proxy", new ProxyConfiguration()], 503 ["setWindowRect", !lazy.AppInfo.isAndroid], 504 ["unhandledPromptBehavior", new lazy.UserPromptHandler()], 505 ["userAgent", lazy.userAgent], 506 507 // Gecko specific capabilities 508 ["moz:buildID", lazy.AppInfo.appBuildID], 509 ["moz:headless", lazy.isHeadless], 510 ["moz:platformVersion", Services.sysinfo.getProperty("version")], 511 ["moz:processID", lazy.AppInfo.processID], 512 ["moz:profile", maybeProfile()], 513 ["moz:shutdownTimeout", lazy.shutdownTimeout], 514 ]; 515 516 if (!isBidi) { 517 // HTTP-only capabilities 518 defaults.push( 519 ["pageLoadStrategy", PageLoadStrategy.Normal], 520 ["timeouts", new Timeouts()], 521 ["strictFileInteractability", false], 522 523 // Gecko specific capabilities 524 ["moz:accessibilityChecks", false], 525 ["moz:webdriverClick", true], 526 ["moz:windowless", false] 527 ); 528 } 529 530 super(defaults); 531 } 532 533 /** 534 * @param {string} key 535 * Capability key. 536 * @param {(string|number|boolean)} value 537 * JSON-safe capability value. 538 */ 539 set(key, value) { 540 if (key === "timeouts" && !(value instanceof Timeouts)) { 541 throw new TypeError(); 542 } else if (key === "proxy" && !(value instanceof ProxyConfiguration)) { 543 throw new TypeError(); 544 } 545 546 return super.set(key, value); 547 } 548 549 toString() { 550 return "[object Capabilities]"; 551 } 552 553 /** 554 * JSON serialization of capabilities object. 555 * 556 * @returns {Record<string, ?>} 557 */ 558 toJSON() { 559 let marshalled = marshal(this); 560 561 // Always return the proxy capability even if it's empty 562 if (!("proxy" in marshalled)) { 563 marshalled.proxy = {}; 564 } 565 566 marshalled.timeouts = super.get("timeouts"); 567 marshalled.unhandledPromptBehavior = super.get("unhandledPromptBehavior"); 568 569 return marshalled; 570 } 571 572 /** 573 * Unmarshal a JSON object representation of WebDriver capabilities. 574 * 575 * @param {Record<string, *>=} json 576 * WebDriver capabilities. 577 * @param {boolean=} isBidi 578 * Flag indicating that it is a WebDriver BiDi session. Defaults to false. 579 * 580 * @returns {Capabilities} 581 * Internal representation of WebDriver capabilities. 582 */ 583 static fromJSON(json, isBidi = false) { 584 if (typeof json == "undefined" || json === null) { 585 json = {}; 586 } 587 lazy.assert.object( 588 json, 589 lazy.pprint`Expected "capabilities" to be an object, got ${json}"` 590 ); 591 592 const capabilities = new Capabilities(isBidi); 593 594 // TODO: Bug 1823907. We can start using here spec compliant method `validate`, 595 // as soon as `desiredCapabilities` and `requiredCapabilities` are not supported. 596 for (let [k, v] of Object.entries(json)) { 597 if (isBidi && WEBDRIVER_CLASSIC_CAPABILITIES.includes(k)) { 598 // Ignore any WebDriver classic capability for a WebDriver BiDi session. 599 continue; 600 } 601 602 switch (k) { 603 case "acceptInsecureCerts": 604 lazy.assert.boolean( 605 v, 606 `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}` 607 ); 608 break; 609 610 case "pageLoadStrategy": 611 lazy.assert.string( 612 v, 613 `Expected "${k}" to be a string, ` + lazy.pprint`got ${v}` 614 ); 615 if (!Object.values(PageLoadStrategy).includes(v)) { 616 throw new lazy.error.InvalidArgumentError( 617 "Unknown page load strategy: " + v 618 ); 619 } 620 break; 621 622 case "proxy": 623 v = ProxyConfiguration.fromJSON(v); 624 break; 625 626 case "setWindowRect": 627 lazy.assert.boolean( 628 v, 629 `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}` 630 ); 631 if (!lazy.AppInfo.isAndroid && !v) { 632 throw new lazy.error.InvalidArgumentError( 633 "setWindowRect cannot be disabled" 634 ); 635 } else if (lazy.AppInfo.isAndroid && v) { 636 throw new lazy.error.InvalidArgumentError( 637 "setWindowRect is only supported on desktop" 638 ); 639 } 640 break; 641 642 case "timeouts": 643 v = Timeouts.fromJSON(v); 644 break; 645 646 case "strictFileInteractability": 647 v = lazy.assert.boolean( 648 v, 649 `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}` 650 ); 651 break; 652 653 case "unhandledPromptBehavior": 654 v = lazy.UserPromptHandler.fromJSON(v); 655 break; 656 657 case "webSocketUrl": 658 lazy.assert.boolean( 659 v, 660 `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}` 661 ); 662 663 if (!v) { 664 throw new lazy.error.InvalidArgumentError( 665 `Expected "${k}" to be true, ` + lazy.pprint`got ${v}` 666 ); 667 } 668 break; 669 670 case "webauthn:virtualAuthenticators": 671 lazy.assert.boolean( 672 v, 673 `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}` 674 ); 675 break; 676 677 case "webauthn:extension:uvm": 678 lazy.assert.boolean( 679 v, 680 `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}` 681 ); 682 break; 683 684 case "webauthn:extension:prf": 685 lazy.assert.boolean( 686 v, 687 `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}` 688 ); 689 break; 690 691 case "webauthn:extension:largeBlob": 692 lazy.assert.boolean( 693 v, 694 `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}` 695 ); 696 break; 697 698 case "webauthn:extension:credBlob": 699 lazy.assert.boolean( 700 v, 701 `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}` 702 ); 703 break; 704 705 case "moz:accessibilityChecks": 706 lazy.assert.boolean( 707 v, 708 `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}` 709 ); 710 break; 711 712 case "moz:webdriverClick": 713 lazy.assert.boolean( 714 v, 715 `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}` 716 ); 717 break; 718 719 case "moz:windowless": 720 lazy.assert.boolean( 721 v, 722 `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}` 723 ); 724 725 // Only supported on MacOS 726 if (v && !lazy.AppInfo.isMac) { 727 throw new lazy.error.InvalidArgumentError( 728 "moz:windowless only supported on MacOS" 729 ); 730 } 731 break; 732 } 733 capabilities.set(k, v); 734 } 735 736 return capabilities; 737 } 738 739 /** 740 * Validate WebDriver capability. 741 * 742 * @param {string} name 743 * The name of capability. 744 * @param {string} value 745 * The value of capability. 746 * 747 * @throws {InvalidArgumentError} 748 * If <var>value</var> doesn't pass validation, 749 * which depends on <var>name</var>. 750 * 751 * @returns {string} 752 * The validated capability value. 753 */ 754 static validate(name, value) { 755 if (value === null) { 756 return value; 757 } 758 switch (name) { 759 case "acceptInsecureCerts": 760 lazy.assert.boolean( 761 value, 762 `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}` 763 ); 764 return value; 765 766 case "browserName": 767 case "browserVersion": 768 case "platformName": 769 return lazy.assert.string( 770 value, 771 `Expected "${name}" to be a string, ` + lazy.pprint`got ${value}` 772 ); 773 774 case "pageLoadStrategy": 775 lazy.assert.string( 776 value, 777 `Expected "${name}" to be a string, ` + lazy.pprint`got ${value}` 778 ); 779 if (!Object.values(PageLoadStrategy).includes(value)) { 780 throw new lazy.error.InvalidArgumentError( 781 "Unknown page load strategy: " + value 782 ); 783 } 784 return value; 785 786 case "proxy": 787 return ProxyConfiguration.fromJSON(value); 788 789 case "strictFileInteractability": 790 return lazy.assert.boolean( 791 value, 792 `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}` 793 ); 794 795 case "timeouts": 796 return Timeouts.fromJSON(value); 797 798 case "unhandledPromptBehavior": 799 return lazy.UserPromptHandler.fromJSON(value); 800 801 case "webSocketUrl": 802 lazy.assert.boolean( 803 value, 804 `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}` 805 ); 806 807 if (!value) { 808 throw new lazy.error.InvalidArgumentError( 809 `Expected "${name}" to be true, ` + lazy.pprint`got ${value}` 810 ); 811 } 812 return value; 813 814 case "webauthn:virtualAuthenticators": 815 lazy.assert.boolean( 816 value, 817 `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}` 818 ); 819 return value; 820 821 case "webauthn:extension:uvm": 822 lazy.assert.boolean( 823 value, 824 `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}` 825 ); 826 return value; 827 828 case "webauthn:extension:largeBlob": 829 lazy.assert.boolean( 830 value, 831 `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}` 832 ); 833 return value; 834 835 case "moz:firefoxOptions": 836 return lazy.assert.object( 837 value, 838 `Expected "${name}" to be an object, ` + lazy.pprint`got ${value}` 839 ); 840 841 case "moz:accessibilityChecks": 842 return lazy.assert.boolean( 843 value, 844 `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}` 845 ); 846 847 case "moz:webdriverClick": 848 return lazy.assert.boolean( 849 value, 850 `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}` 851 ); 852 853 case "moz:windowless": 854 lazy.assert.boolean( 855 value, 856 `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}` 857 ); 858 859 // Only supported on MacOS 860 if (value && !lazy.AppInfo.isMac) { 861 throw new lazy.error.InvalidArgumentError( 862 "moz:windowless only supported on MacOS" 863 ); 864 } 865 return value; 866 867 default: 868 lazy.assert.string( 869 name, 870 `Expected capability "name" to be a string, ` + 871 lazy.pprint`got ${name}` 872 ); 873 if (name.includes(":")) { 874 const [prefix] = name.split(":"); 875 if (prefix !== "moz") { 876 return value; 877 } 878 } 879 throw new lazy.error.InvalidArgumentError( 880 `${name} is not the name of a known capability or extension capability` 881 ); 882 } 883 } 884 } 885 886 function getWebDriverBrowserName() { 887 // Similar to chromedriver which reports "chrome" as browser name for all 888 // WebView apps, we will report "firefox" for all GeckoView apps. 889 if (lazy.AppInfo.isAndroid) { 890 return "firefox"; 891 } 892 893 return lazy.AppInfo.name?.toLowerCase(); 894 } 895 896 function getWebDriverPlatformName() { 897 let name = Services.sysinfo.getProperty("name"); 898 899 if (lazy.AppInfo.isAndroid) { 900 return "android"; 901 } 902 903 switch (name) { 904 case "Windows_NT": 905 return "windows"; 906 907 case "Darwin": 908 return "mac"; 909 910 default: 911 return name.toLowerCase(); 912 } 913 } 914 915 // Specialisation of |JSON.stringify| that produces JSON-safe object 916 // literals, dropping empty objects and entries which values are undefined 917 // or null. Objects are allowed to produce their own JSON representations 918 // by implementing a |toJSON| function. 919 function marshal(obj) { 920 let rv = Object.create(null); 921 922 function* iter(mapOrObject) { 923 if (mapOrObject instanceof Map) { 924 for (const [k, v] of mapOrObject) { 925 yield [k, v]; 926 } 927 } else { 928 for (const k of Object.keys(mapOrObject)) { 929 yield [k, mapOrObject[k]]; 930 } 931 } 932 } 933 934 for (let [k, v] of iter(obj)) { 935 // Skip empty values when serialising to JSON. 936 if (typeof v == "undefined" || v === null) { 937 continue; 938 } 939 940 // Recursively marshal objects that are able to produce their own 941 // JSON representation. 942 if (typeof v.toJSON == "function") { 943 v = marshal(v.toJSON()); 944 945 // Or do the same for object literals. 946 } else if (isObject(v)) { 947 v = marshal(v); 948 } 949 950 // And finally drop (possibly marshaled) objects which have no 951 // entries. 952 if (!isObjectEmpty(v)) { 953 rv[k] = v; 954 } 955 } 956 957 return rv; 958 } 959 960 function isObject(obj) { 961 return Object.prototype.toString.call(obj) == "[object Object]"; 962 } 963 964 function isObjectEmpty(obj) { 965 return isObject(obj) && Object.keys(obj).length === 0; 966 } 967 968 // Services.dirsvc is not accessible from JSWindowActor child, 969 // but we should not panic about that. 970 function maybeProfile() { 971 try { 972 return Services.dirsvc.get("ProfD", Ci.nsIFile).path; 973 } catch (e) { 974 return "<protected>"; 975 } 976 } 977 978 /** 979 * Merge WebDriver capabilities. 980 * 981 * @see https://w3c.github.io/webdriver/#dfn-merging-capabilities 982 * 983 * @param {object} primary 984 * Required capabilities which need to be merged with <var>secondary</var>. 985 * @param {object=} secondary 986 * Secondary capabilities. 987 * 988 * @returns {object} Merged capabilities. 989 * 990 * @throws {InvalidArgumentError} 991 * If <var>primary</var> and <var>secondary</var> have the same keys. 992 */ 993 export function mergeCapabilities(primary, secondary) { 994 const result = { ...primary }; 995 996 if (secondary === undefined) { 997 return result; 998 } 999 1000 Object.entries(secondary).forEach(([name, value]) => { 1001 if (primary[name] !== undefined) { 1002 // Since at the moment we always pass as `primary` `alwaysMatch` object 1003 // and as `secondary` an item from `firstMatch` array from `capabilities`, 1004 // we can make this error message more specific. 1005 throw new lazy.error.InvalidArgumentError( 1006 `firstMatch key ${name} shadowed a value in alwaysMatch` 1007 ); 1008 } 1009 result[name] = value; 1010 }); 1011 1012 return result; 1013 } 1014 1015 /** 1016 * Validate WebDriver capabilities. 1017 * 1018 * @see https://w3c.github.io/webdriver/#dfn-validate-capabilities 1019 * 1020 * @param {object} capabilities 1021 * Capabilities which need to be validated. 1022 * 1023 * @returns {object} Validated capabilities. 1024 * 1025 * @throws {InvalidArgumentError} 1026 * If <var>capabilities</var> is not an object. 1027 */ 1028 export function validateCapabilities(capabilities) { 1029 lazy.assert.object( 1030 capabilities, 1031 lazy.pprint`Expected "capabilities" to be an object, got ${capabilities}` 1032 ); 1033 1034 const result = {}; 1035 1036 Object.entries(capabilities).forEach(([name, value]) => { 1037 const deserialized = Capabilities.validate(name, value); 1038 if (deserialized !== null) { 1039 if (["proxy", "timeouts", "unhandledPromptBehavior"].includes(name)) { 1040 // Return pure values for objects that will be setup during session creation. 1041 result[name] = value; 1042 } else { 1043 result[name] = deserialized; 1044 } 1045 } 1046 }); 1047 1048 return result; 1049 } 1050 1051 /** 1052 * Process WebDriver capabilities. 1053 * 1054 * @see https://w3c.github.io/webdriver/#processing-capabilities 1055 * 1056 * @param {object} params 1057 * @param {object} params.capabilities 1058 * Capabilities which need to be processed. 1059 * 1060 * @returns {object} Processed capabilities. 1061 * 1062 * @throws {InvalidArgumentError} 1063 * If <var>capabilities</var> do not satisfy the criteria. 1064 */ 1065 export function processCapabilities(params) { 1066 const { capabilities } = params; 1067 lazy.assert.object( 1068 capabilities, 1069 lazy.pprint`Expected "capabilities" to be an object, got ${capabilities}` 1070 ); 1071 1072 let { 1073 alwaysMatch: requiredCapabilities = {}, 1074 firstMatch: allFirstMatchCapabilities = [{}], 1075 } = capabilities; 1076 1077 requiredCapabilities = validateCapabilities(requiredCapabilities); 1078 1079 lazy.assert.isNonEmptyArray( 1080 allFirstMatchCapabilities, 1081 lazy.pprint`Expected "firstMatch" to be a non-empty array, got ${allFirstMatchCapabilities}` 1082 ); 1083 1084 const validatedFirstMatchCapabilities = 1085 allFirstMatchCapabilities.map(validateCapabilities); 1086 1087 const mergedCapabilities = []; 1088 validatedFirstMatchCapabilities.forEach(firstMatchCapabilities => { 1089 const merged = mergeCapabilities( 1090 requiredCapabilities, 1091 firstMatchCapabilities 1092 ); 1093 mergedCapabilities.push(merged); 1094 }); 1095 1096 // TODO: Bug 1836288. Implement the capability matching logic 1097 // for "browserName", "browserVersion", "platformName", and 1098 // "unhandledPromptBehavior" features, 1099 // for now we can just pick the first merged capability. 1100 const matchedCapabilities = mergedCapabilities[0]; 1101 1102 return matchedCapabilities; 1103 }