DevToolsUtils.js (31855B)
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 /* globals setImmediate, rpc */ 6 7 "use strict"; 8 9 /* General utilities used throughout devtools. */ 10 11 var flags = require("resource://devtools/shared/flags.js"); 12 var { 13 getStack, 14 callFunctionWithAsyncStack, 15 } = require("resource://devtools/shared/platform/stack.js"); 16 17 const lazy = {}; 18 19 if (!isWorker) { 20 ChromeUtils.defineESModuleGetters( 21 lazy, 22 { 23 DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs", 24 FileUtils: "resource://gre/modules/FileUtils.sys.mjs", 25 NetworkHelper: 26 "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs", 27 ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", 28 }, 29 { global: "contextual" } 30 ); 31 } 32 33 // Native getters which are considered to be side effect free. 34 ChromeUtils.defineLazyGetter(lazy, "sideEffectFreeGetters", () => { 35 const { 36 getters, 37 } = require("resource://devtools/server/actors/webconsole/eager-ecma-allowlist.js"); 38 39 const map = new Map(); 40 for (const n of getters) { 41 if (!map.has(n.name)) { 42 map.set(n.name, []); 43 } 44 map.get(n.name).push(n); 45 } 46 47 return map; 48 }); 49 50 // Using this name lets the eslint plugin know about lazy defines in 51 // this file. 52 var DevToolsUtils = exports; 53 54 // Re-export the thread-safe utils. 55 const ThreadSafeDevToolsUtils = require("resource://devtools/shared/ThreadSafeDevToolsUtils.js"); 56 for (const key of Object.keys(ThreadSafeDevToolsUtils)) { 57 exports[key] = ThreadSafeDevToolsUtils[key]; 58 } 59 60 /** 61 * Waits for the next tick in the event loop to execute a callback. 62 */ 63 exports.executeSoon = function (fn) { 64 if (isWorker) { 65 setImmediate(fn); 66 } else { 67 let executor; 68 // Only enable async stack reporting when DEBUG_JS_MODULES is set 69 // (customized local builds) to avoid a performance penalty. 70 if (AppConstants.DEBUG_JS_MODULES || flags.testing) { 71 const stack = getStack(); 72 executor = () => { 73 callFunctionWithAsyncStack(fn, stack, "DevToolsUtils.executeSoon"); 74 }; 75 } else { 76 executor = fn; 77 } 78 Services.tm.dispatchToMainThread({ 79 run: exports.makeInfallible(executor), 80 }); 81 } 82 }; 83 84 /** 85 * Similar to executeSoon, but enters microtask before executing the callback 86 * if this is called on the main thread. 87 */ 88 exports.executeSoonWithMicroTask = function (fn) { 89 if (isWorker) { 90 setImmediate(fn); 91 } else { 92 let executor; 93 // Only enable async stack reporting when DEBUG_JS_MODULES is set 94 // (customized local builds) to avoid a performance penalty. 95 if (AppConstants.DEBUG_JS_MODULES || flags.testing) { 96 const stack = getStack(); 97 executor = () => { 98 callFunctionWithAsyncStack( 99 fn, 100 stack, 101 "DevToolsUtils.executeSoonWithMicroTask" 102 ); 103 }; 104 } else { 105 executor = fn; 106 } 107 Services.tm.dispatchToMainThreadWithMicroTask({ 108 run: exports.makeInfallible(executor), 109 }); 110 } 111 }; 112 113 /** 114 * Waits for the next tick in the event loop. 115 * 116 * @return Promise 117 * A promise that is resolved after the next tick in the event loop. 118 */ 119 exports.waitForTick = function () { 120 return new Promise(resolve => { 121 exports.executeSoon(resolve); 122 }); 123 }; 124 125 /** 126 * Waits for the specified amount of time to pass. 127 * 128 * @param number delay 129 * The amount of time to wait, in milliseconds. 130 * @return Promise 131 * A promise that is resolved after the specified amount of time passes. 132 */ 133 exports.waitForTime = function (delay) { 134 return new Promise(resolve => setTimeout(resolve, delay)); 135 }; 136 137 /** 138 * Like ChromeUtils.defineLazyGetter, but with a |this| sensitive getter that 139 * allows the lazy getter to be defined on a prototype and work correctly with 140 * instances. 141 * 142 * @param Object object 143 * The prototype object to define the lazy getter on. 144 * @param String key 145 * The key to define the lazy getter on. 146 * @param Function callback 147 * The callback that will be called to determine the value. Will be 148 * called with the |this| value of the current instance. 149 */ 150 exports.defineLazyPrototypeGetter = function (object, key, callback) { 151 Object.defineProperty(object, key, { 152 configurable: true, 153 get() { 154 const value = callback.call(this); 155 156 Object.defineProperty(this, key, { 157 configurable: true, 158 writable: true, 159 value, 160 }); 161 162 return value; 163 }, 164 }); 165 }; 166 167 /** 168 * Safely get the property value from a Debugger.Object for a given key. Walks 169 * the prototype chain until the property is found. 170 * 171 * @param {Debugger.Object} object 172 * The Debugger.Object to get the value from. 173 * @param {string} key 174 * The key to look for. 175 * @param {boolean} invokeUnsafeGetter (defaults to false). 176 * Optional boolean to indicate if the function should execute unsafe getter 177 * in order to retrieve its result's properties. 178 * ⚠️ This should be set to true *ONLY* on user action as it may cause side-effects 179 * in the content page ⚠️ 180 * @return Any 181 */ 182 exports.getProperty = function (object, key, invokeUnsafeGetters = false) { 183 const root = object; 184 while (object && exports.isSafeDebuggerObject(object)) { 185 let desc; 186 try { 187 desc = object.getOwnPropertyDescriptor(key); 188 } catch (e) { 189 // The above can throw when the debuggee does not subsume the object's 190 // compartment, or for some WrappedNatives like Cu.Sandbox. 191 return undefined; 192 } 193 if (desc) { 194 if ("value" in desc) { 195 return desc.value; 196 } 197 // Call the getter if it's safe. 198 if (exports.hasSafeGetter(desc) || invokeUnsafeGetters === true) { 199 try { 200 return desc.get.call(root).return; 201 } catch (e) { 202 // If anything goes wrong report the error and return undefined. 203 exports.reportException("getProperty", e); 204 } 205 } 206 return undefined; 207 } 208 object = object.proto; 209 } 210 return undefined; 211 }; 212 213 /** 214 * Removes all the non-opaque security wrappers of a debuggee object. 215 * 216 * @param obj Debugger.Object 217 * The debuggee object to be unwrapped. 218 * @return Debugger.Object|null|undefined 219 * - If the object has no wrapper, the same `obj` is returned. Note DeadObject 220 * objects belong to this case. 221 * - Otherwise, if the debuggee doesn't subsume object's compartment, returns `null`. 222 * - Otherwise, if the object belongs to an invisible-to-debugger compartment, 223 * returns `undefined`. 224 * - Otherwise, returns the unwrapped object. 225 */ 226 exports.unwrap = function unwrap(obj) { 227 // Check if `obj` has an opaque wrapper. 228 if (obj.class === "Opaque") { 229 return obj; 230 } 231 232 // Attempt to unwrap via `obj.unwrap()`. Note that: 233 // - This will return `null` if the debuggee does not subsume object's compartment. 234 // - This will throw if the object belongs to an invisible-to-debugger compartment. 235 // - This will return `obj` if there is no wrapper. 236 let unwrapped; 237 try { 238 unwrapped = obj.unwrap(); 239 } catch (err) { 240 return undefined; 241 } 242 243 // Check if further unwrapping is not possible. 244 if (!unwrapped || unwrapped === obj) { 245 return unwrapped; 246 } 247 248 // Recursively remove additional security wrappers. 249 return unwrap(unwrapped); 250 }; 251 252 /** 253 * Checks whether a debuggee object is safe. Unsafe objects may run proxy traps or throw 254 * when using `proto`, `isExtensible`, `isFrozen` or `isSealed`. Note that safe objects 255 * may still throw when calling `getOwnPropertyNames`, `getOwnPropertyDescriptor`, etc. 256 * Also note DeadObject objects are considered safe. 257 * 258 * @param obj Debugger.Object 259 * The debuggee object to be checked. 260 * @return boolean 261 */ 262 exports.isSafeDebuggerObject = function (obj) { 263 const unwrapped = exports.unwrap(obj); 264 265 // Objects belonging to an invisible-to-debugger compartment might be proxies, 266 // so just in case consider them unsafe. 267 if (unwrapped === undefined) { 268 return false; 269 } 270 271 // If the debuggee does not subsume the object's compartment, most properties won't 272 // be accessible. Cross-origin Window and Location objects might expose some, though. 273 // Therefore, it must be considered safe. Note that proxy objects have fully opaque 274 // security wrappers, so proxy traps won't run in this case. 275 if (unwrapped === null) { 276 return true; 277 } 278 279 // Proxy objects can run traps when accessed. `isProxy` getter is called on `unwrapped` 280 // instead of on `obj` in order to detect proxies behind transparent wrappers. 281 if (unwrapped.isProxy) { 282 return false; 283 } 284 285 return true; 286 }; 287 288 /** 289 * Determines if a descriptor has a getter which doesn't call into JavaScript. 290 * 291 * @param Object desc 292 * The descriptor to check for a safe getter. 293 * @return Boolean 294 * Whether a safe getter was found. 295 */ 296 exports.hasSafeGetter = function (desc) { 297 // Scripted functions that are CCWs will not appear scripted until after 298 // unwrapping. 299 let fn = desc.get; 300 fn = fn && exports.unwrap(fn); 301 if (!fn) { 302 return false; 303 } 304 if (!fn.callable || fn.class !== "Function") { 305 return false; 306 } 307 if (fn.script !== undefined) { 308 // This is scripted function. 309 return false; 310 } 311 312 // This is a getter with native function. 313 314 // We assume all DOM getters have no major side effect, and they are 315 // eagerly-evaluateable. 316 // 317 // JitInfo is used only by methods/accessors in WebIDL, and being 318 // "a getter with JitInfo" can be used as a condition to check if given 319 // function is DOM getter. 320 // 321 // This includes privileged interfaces in addition to standard web APIs. 322 if (fn.isNativeGetterWithJitInfo()) { 323 return true; 324 } 325 326 // Apply explicit allowlist. 327 const natives = lazy.sideEffectFreeGetters.get(fn.name); 328 return natives && natives.some(n => fn.isSameNative(n)); 329 }; 330 331 /** 332 * Check that the property value from a Debugger.Object for a given key is an unsafe 333 * getter or not. Walks the prototype chain until the property is found. 334 * 335 * @param {Debugger.Object} object 336 * The Debugger.Object to check on. 337 * @param {string} key 338 * The key to look for. 339 * @param {boolean} invokeUnsafeGetter (defaults to false). 340 * Optional boolean to indicate if the function should execute unsafe getter 341 * in order to retrieve its result's properties. 342 * @return Boolean 343 */ 344 exports.isUnsafeGetter = function (object, key) { 345 while (object && exports.isSafeDebuggerObject(object)) { 346 let desc; 347 try { 348 desc = object.getOwnPropertyDescriptor(key); 349 } catch (e) { 350 // The above can throw when the debuggee does not subsume the object's 351 // compartment, or for some WrappedNatives like Cu.Sandbox. 352 return false; 353 } 354 if (desc) { 355 if (Object.getOwnPropertyNames(desc).includes("get")) { 356 return !exports.hasSafeGetter(desc); 357 } 358 } 359 object = object.proto; 360 } 361 362 return false; 363 }; 364 365 /** 366 * Check if it is safe to read properties and execute methods from the given JS 367 * object. Safety is defined as being protected from unintended code execution 368 * from content scripts (or cross-compartment code). 369 * 370 * See bugs 945920 and 946752 for discussion. 371 * 372 * @type Object obj 373 * The object to check. 374 * @return Boolean 375 * True if it is safe to read properties from obj, or false otherwise. 376 */ 377 exports.isSafeJSObject = function (obj) { 378 // If we are running on a worker thread, Cu is not available. In this case, 379 // we always return false, just to be on the safe side. 380 if (isWorker) { 381 return false; 382 } 383 384 if ( 385 Cu.getGlobalForObject(obj) == Cu.getGlobalForObject(exports.isSafeJSObject) 386 ) { 387 // obj is not a cross-compartment wrapper. 388 return true; 389 } 390 391 // Xray wrappers protect against unintended code execution. 392 if (Cu.isXrayWrapper(obj)) { 393 return true; 394 } 395 396 // If there aren't Xrays, only allow chrome objects. 397 const principal = Cu.getObjectPrincipal(obj); 398 if (!principal.isSystemPrincipal) { 399 return false; 400 } 401 402 // Scripted proxy objects without Xrays can run their proxy traps. 403 if (Cu.isProxy(obj)) { 404 return false; 405 } 406 407 // Even if `obj` looks safe, an unsafe object in its prototype chain may still 408 // run unintended code, e.g. when using the `instanceof` operator. 409 const proto = Object.getPrototypeOf(obj); 410 if (proto && !exports.isSafeJSObject(proto)) { 411 return false; 412 } 413 414 // Allow non-problematic chrome objects. 415 return true; 416 }; 417 418 /** 419 * Dump with newline - This is a logging function that will only output when 420 * the preference "devtools.debugger.log" is set to true. Typically it is used 421 * for logging the remote debugging protocol calls. 422 */ 423 exports.dumpn = function (str) { 424 if (flags.wantLogging) { 425 dump("DBG-SERVER: " + str + "\n"); 426 } 427 }; 428 429 /** 430 * Dump verbose - This is a verbose logger for low-level tracing, that is typically 431 * used to provide information about the remote debugging protocol's transport 432 * mechanisms. The logging can be enabled by changing the preferences 433 * "devtools.debugger.log" and "devtools.debugger.log.verbose" to true. 434 */ 435 exports.dumpv = function (msg) { 436 if (flags.wantVerbose) { 437 exports.dumpn(msg); 438 } 439 }; 440 441 /** 442 * Defines a getter on a specified object that will be created upon first use. 443 * 444 * @param object 445 * The object to define the lazy getter on. 446 * @param name 447 * The name of the getter to define on object. 448 * @param lambda 449 * A function that returns what the getter should return. This will 450 * only ever be called once. 451 */ 452 exports.defineLazyGetter = function (object, name, lambda) { 453 Object.defineProperty(object, name, { 454 get() { 455 delete object[name]; 456 object[name] = lambda.apply(object); 457 return object[name]; 458 }, 459 configurable: true, 460 enumerable: true, 461 }); 462 }; 463 464 DevToolsUtils.defineLazyGetter(this, "AppConstants", () => { 465 if (isWorker) { 466 return {}; 467 } 468 return ChromeUtils.importESModule( 469 "resource://gre/modules/AppConstants.sys.mjs", 470 { global: "contextual" } 471 ).AppConstants; 472 }); 473 474 /** 475 * No operation. The empty function. 476 */ 477 exports.noop = function () {}; 478 479 let assertionFailureCount = 0; 480 481 Object.defineProperty(exports, "assertionFailureCount", { 482 get() { 483 return assertionFailureCount; 484 }, 485 }); 486 487 function reallyAssert(condition, message) { 488 if (!condition) { 489 assertionFailureCount++; 490 const err = new Error("Assertion failure: " + message); 491 exports.reportException("DevToolsUtils.assert", err); 492 throw err; 493 } 494 } 495 496 /** 497 * DevToolsUtils.assert(condition, message) 498 * 499 * @param Boolean condition 500 * @param String message 501 * 502 * Assertions are enabled when any of the following are true: 503 * - This is a DEBUG_JS_MODULES build 504 * - flags.testing is set to true 505 * 506 * If assertions are enabled, then `condition` is checked and if false-y, the 507 * assertion failure is logged and then an error is thrown. 508 * 509 * If assertions are not enabled, then this function is a no-op. 510 */ 511 Object.defineProperty(exports, "assert", { 512 get: () => 513 AppConstants.DEBUG_JS_MODULES || flags.testing 514 ? reallyAssert 515 : exports.noop, 516 }); 517 518 DevToolsUtils.defineLazyGetter(this, "NetUtil", () => { 519 return ChromeUtils.importESModule("resource://gre/modules/NetUtil.sys.mjs", { 520 global: "contextual", 521 }).NetUtil; 522 }); 523 524 /** 525 * Performs a request to load the desired URL and returns a promise. 526 * 527 * @param urlIn String 528 * The URL we will request. 529 * @param aOptions Object 530 * An object with the following optional properties: 531 * - loadFromCache: if false, will bypass the cache and 532 * always load fresh from the network (default: true) 533 * - policy: the nsIContentPolicy type to apply when fetching the URL 534 * (only works when loading from system principal) 535 * - window: the window to get the loadGroup from 536 * - charset: the charset to use if the channel doesn't provide one 537 * - principal: the principal to use, if omitted, the request is loaded 538 * with a content principal corresponding to the url being 539 * loaded, using the origin attributes of the window, if any. 540 * - headers: extra headers 541 * - cacheKey: when loading from cache, use this key to retrieve a cache 542 * specific to a given SHEntry. (Allows loading POST 543 * requests from cache) 544 * @returns Promise that resolves with an object with the following members on 545 * success: 546 * - content: the document at that URL, as a string, 547 * - contentType: the content type of the document 548 * 549 * If an error occurs, the promise is rejected with that error. 550 * 551 * XXX: It may be better to use nsITraceableChannel to get to the sources 552 * without relying on caching when we can (not for eval, etc.): 553 * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/ 554 */ 555 function mainThreadFetch( 556 urlIn, 557 aOptions = { 558 loadFromCache: true, 559 policy: Ci.nsIContentPolicy.TYPE_OTHER, 560 window: null, 561 charset: null, 562 principal: null, 563 headers: null, 564 cacheKey: 0, 565 } 566 ) { 567 return new Promise((resolve, reject) => { 568 // Create a channel. 569 const url = urlIn.split(" -> ").pop(); 570 let channel; 571 try { 572 channel = newChannelForURL(url, aOptions); 573 } catch (ex) { 574 reject(ex); 575 return; 576 } 577 578 channel.loadInfo.isInDevToolsContext = true; 579 580 // Set the channel options. 581 channel.loadFlags = aOptions.loadFromCache 582 ? channel.LOAD_FROM_CACHE 583 : channel.LOAD_BYPASS_CACHE; 584 585 if (aOptions.loadFromCache && channel instanceof Ci.nsICacheInfoChannel) { 586 // If DevTools intents to load the content from the cache, 587 // we make the LOAD_FROM_CACHE flag preferred over LOAD_BYPASS_CACHE. 588 channel.preferCacheLoadOverBypass = true; 589 590 // When loading from cache, the cacheKey allows us to target a specific 591 // SHEntry and offer ways to restore POST requests from cache. 592 if (aOptions.cacheKey != 0) { 593 channel.cacheKey = aOptions.cacheKey; 594 } 595 } 596 597 if (aOptions.headers && channel instanceof Ci.nsIHttpChannel) { 598 for (const h in aOptions.headers) { 599 channel.setRequestHeader(h, aOptions.headers[h], /* aMerge = */ false); 600 } 601 } 602 603 if (aOptions.window) { 604 // Respect private browsing. 605 channel.loadGroup = aOptions.window.docShell.QueryInterface( 606 Ci.nsIDocumentLoader 607 ).loadGroup; 608 } 609 610 // eslint-disable-next-line complexity 611 const onResponse = (stream, status, request) => { 612 if (!Components.isSuccessCode(status)) { 613 reject(new Error(`Failed to fetch ${url}. Code ${status}.`)); 614 return; 615 } 616 617 try { 618 // We cannot use NetUtil to do the charset conversion as if charset 619 // information is not available and our default guess is wrong the method 620 // might fail and we lose the stream data. This means we can't fall back 621 // to using the locale default encoding (bug 1181345). 622 623 // Read and decode the data according to the locale default encoding. 624 625 let available; 626 try { 627 available = stream.available(); 628 } catch (ex) { 629 if (ex.name === "NS_BASE_STREAM_CLOSED") { 630 // Empty files cause NS_BASE_STREAM_CLOSED exception. 631 // If there was a real stream error, we would have already rejected above. 632 resolve({ 633 content: "", 634 contentType: "text/plain", 635 }); 636 return; 637 } 638 639 reject(ex); 640 } 641 let source = NetUtil.readInputStreamToString(stream, available); 642 stream.close(); 643 644 // We do our own BOM sniffing here because there's no convenient 645 // implementation of the "decode" algorithm 646 // (https://encoding.spec.whatwg.org/#decode) exposed to JS. 647 let bomCharset = null; 648 if ( 649 available >= 3 && 650 source.codePointAt(0) == 0xef && 651 source.codePointAt(1) == 0xbb && 652 source.codePointAt(2) == 0xbf 653 ) { 654 bomCharset = "UTF-8"; 655 source = source.slice(3); 656 } else if ( 657 available >= 2 && 658 source.codePointAt(0) == 0xfe && 659 source.codePointAt(1) == 0xff 660 ) { 661 bomCharset = "UTF-16BE"; 662 source = source.slice(2); 663 } else if ( 664 available >= 2 && 665 source.codePointAt(0) == 0xff && 666 source.codePointAt(1) == 0xfe 667 ) { 668 bomCharset = "UTF-16LE"; 669 source = source.slice(2); 670 } 671 672 // If the channel or the caller has correct charset information, the 673 // content will be decoded correctly. If we have to fall back to UTF-8 and 674 // the guess is wrong, the conversion fails and convertToUnicode returns 675 // the input unmodified. Essentially we try to decode the data as UTF-8 676 // and if that fails, we use the locale specific default encoding. This is 677 // the best we can do if the source does not provide charset info. 678 let charset = bomCharset; 679 if (!charset) { 680 try { 681 charset = channel.contentCharset; 682 } catch (e) { 683 // Accessing `contentCharset` on content served by a service worker in 684 // non-e10s may throw. 685 } 686 } 687 if (!charset) { 688 charset = aOptions.charset || "UTF-8"; 689 } 690 const unicodeSource = lazy.NetworkHelper.convertToUnicode( 691 source, 692 charset 693 ); 694 695 // Look for any source map URL in the response. 696 let sourceMapURL; 697 if (request instanceof Ci.nsIHttpChannel) { 698 try { 699 sourceMapURL = request.getResponseHeader("SourceMap"); 700 } catch (e) {} 701 if (!sourceMapURL) { 702 try { 703 sourceMapURL = request.getResponseHeader("X-SourceMap"); 704 } catch (e) {} 705 } 706 } 707 708 resolve({ 709 content: unicodeSource, 710 contentType: request.contentType, 711 sourceMapURL, 712 }); 713 } catch (ex) { 714 reject(ex); 715 } 716 }; 717 718 // Open the channel 719 try { 720 NetUtil.asyncFetch(channel, onResponse); 721 } catch (ex) { 722 reject(ex); 723 } 724 }); 725 } 726 727 /** 728 * Opens a channel for given URL. Tries a bit harder than NetUtil.newChannel. 729 * 730 * @param {string} url - The URL to open a channel for. 731 * @param {object} options - The options object passed to @method fetch. 732 * @return {nsIChannel} - The newly created channel. Throws on failure. 733 */ 734 function newChannelForURL(url, { policy, window, principal }) { 735 const securityFlags = 736 Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL; 737 738 let uri; 739 try { 740 uri = Services.io.newURI(url); 741 } catch (e) { 742 // In the xpcshell tests, the script url is the absolute path of the test 743 // file, which will make a malformed URI error be thrown. Add the file 744 // scheme to see if it helps. 745 uri = Services.io.newURI("file://" + url); 746 } 747 748 // In xpcshell tests on Windows, opening the channel 749 // can throw NS_ERROR_UNKNOWN_PROTOCOL if the external protocol isn't 750 // supported by Windows, so we also need to handle that case here if 751 // parsing the URL above doesn't throw. 752 const handler = Services.io.getProtocolHandler(uri.scheme); 753 if ( 754 handler instanceof Ci.nsIExternalProtocolHandler && 755 !handler.externalAppExistsForScheme(uri.scheme) 756 ) { 757 uri = Services.io.newURI("file://" + url); 758 } 759 760 const channelOptions = { 761 contentPolicyType: policy, 762 securityFlags, 763 uri, 764 }; 765 766 // Ensure that we have some contentPolicyType type set if one was 767 // not provided. 768 if (!channelOptions.contentPolicyType) { 769 channelOptions.contentPolicyType = Ci.nsIContentPolicy.TYPE_OTHER; 770 } 771 772 // If a window is provided, always use it's document as the loadingNode. 773 // This will provide the correct principal, origin attributes, service 774 // worker controller, etc. 775 if (window) { 776 channelOptions.loadingNode = window.document; 777 } else { 778 // If a window is not provided, then we must set a loading principal. 779 780 // If the caller did not provide a principal, then we use the URI 781 // to create one. Note, it's not clear what use cases require this 782 // and it may not be correct. 783 let prin = principal; 784 if (!prin) { 785 prin = Services.scriptSecurityManager.createContentPrincipal(uri, {}); 786 } 787 788 channelOptions.loadingPrincipal = prin; 789 } 790 791 return NetUtil.newChannel(channelOptions); 792 } 793 794 // Fetch is defined differently depending on whether we are on the main thread 795 // or a worker thread. 796 if (this.isWorker) { 797 // Services is not available in worker threads, nor is there any other way 798 // to fetch a URL. We need to enlist the help from the main thread here, by 799 // issuing an rpc request, to fetch the URL on our behalf. 800 exports.fetch = function (url, options) { 801 return rpc("fetch", url, options); 802 }; 803 } else { 804 exports.fetch = mainThreadFetch; 805 } 806 807 /** 808 * Open the file at the given path for reading. 809 * 810 * @param {string} filePath 811 * 812 * @returns Promise<nsIInputStream> 813 */ 814 exports.openFileStream = function (filePath) { 815 return new Promise((resolve, reject) => { 816 const uri = NetUtil.newURI(new lazy.FileUtils.File(filePath)); 817 NetUtil.asyncFetch( 818 { uri, loadUsingSystemPrincipal: true }, 819 (stream, result) => { 820 if (!Components.isSuccessCode(result)) { 821 reject(new Error(`Could not open "${filePath}": result = ${result}`)); 822 return; 823 } 824 825 resolve(stream); 826 } 827 ); 828 }); 829 }; 830 831 /** 832 * Save the given data to disk after asking the user where to do so. 833 * 834 * @param {Window} parentWindow 835 * The parent window to use to display the filepicker. 836 * @param {UInt8Array} dataArray 837 * The data to write to the file. 838 * @param {string} fileName 839 * The suggested filename. 840 * @param {Array} filters 841 * An array of object of the following shape: 842 * - pattern: A pattern for accepted files (example: "*.js") 843 * - label: The label that will be displayed in the save file dialog. 844 * @return {string | null} 845 * The path to the local saved file, if saved. 846 */ 847 exports.saveAs = async function ( 848 parentWindow, 849 dataArray, 850 fileName = "", 851 filters = [] 852 ) { 853 let returnFile; 854 try { 855 returnFile = await exports.showSaveFileDialog( 856 parentWindow, 857 lazy.DownloadPaths.sanitize(fileName), 858 filters 859 ); 860 } catch (ex) { 861 return null; 862 } 863 864 // Sanitize the filename again in case the user renamed the file to use a 865 // vulnerable extension. 866 returnFile.leafName = lazy.DownloadPaths.sanitize(returnFile.leafName); 867 868 await IOUtils.write(returnFile.path, dataArray, { 869 tmpPath: returnFile.path + ".tmp", 870 }); 871 872 return returnFile.path; 873 }; 874 875 /** 876 * Show file picker and return the file user selected. 877 * 878 * @param {nsIWindow} parentWindow 879 * Optional parent window. If null the parent window of the file picker 880 * will be the window of the attached input element. 881 * @param {string} suggestedFilename 882 * The suggested filename. 883 * @param {Array} filters 884 * An array of object of the following shape: 885 * - pattern: A pattern for accepted files (example: "*.js") 886 * - label: The label that will be displayed in the save file dialog. 887 * @return {Promise} 888 * A promise that is resolved after the file is selected by the file picker 889 */ 890 exports.showSaveFileDialog = function ( 891 parentWindow, 892 suggestedFilename, 893 filters = [] 894 ) { 895 const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); 896 897 if (suggestedFilename) { 898 fp.defaultString = suggestedFilename; 899 } 900 901 fp.init(parentWindow.browsingContext, null, fp.modeSave); 902 if (Array.isArray(filters) && filters.length) { 903 for (const { pattern, label } of filters) { 904 fp.appendFilter(label, pattern); 905 } 906 } else { 907 fp.appendFilters(fp.filterAll); 908 } 909 910 return new Promise((resolve, reject) => { 911 fp.open(result => { 912 if (result == Ci.nsIFilePicker.returnCancel) { 913 reject(); 914 } else { 915 resolve(fp.file); 916 } 917 }); 918 }); 919 }; 920 921 /* 922 * All of the flags have been moved to a different module. Make sure 923 * nobody is accessing them anymore, and don't write new code using 924 * them. We can remove this code after a while. 925 */ 926 function errorOnFlag(exports, name) { 927 Object.defineProperty(exports, name, { 928 get: () => { 929 const msg = 930 `Cannot get the flag ${name}. ` + 931 `Use the "devtools/shared/flags" module instead`; 932 console.error(msg); 933 throw new Error(msg); 934 }, 935 set: () => { 936 const msg = 937 `Cannot set the flag ${name}. ` + 938 `Use the "devtools/shared/flags" module instead`; 939 console.error(msg); 940 throw new Error(msg); 941 }, 942 }); 943 } 944 945 errorOnFlag(exports, "testing"); 946 errorOnFlag(exports, "wantLogging"); 947 errorOnFlag(exports, "wantVerbose"); 948 949 // Calls the property with the given `name` on the given `object`, where 950 // `name` is a string, and `object` a Debugger.Object instance. 951 // 952 // This function uses only the Debugger.Object API to call the property. It 953 // avoids the use of unsafeDeference. This is useful for example in workers, 954 // where unsafeDereference will return an opaque security wrapper to the 955 // referent. 956 function callPropertyOnObject(object, name, ...args) { 957 // Find the property. 958 let descriptor; 959 let proto = object; 960 do { 961 descriptor = proto.getOwnPropertyDescriptor(name); 962 if (descriptor !== undefined) { 963 break; 964 } 965 proto = proto.proto; 966 } while (proto !== null); 967 if (descriptor === undefined) { 968 throw new Error("No such property"); 969 } 970 const value = descriptor.value; 971 if (typeof value !== "object" || value === null || !("callable" in value)) { 972 throw new Error("Not a callable object."); 973 } 974 975 if (value.script !== undefined) { 976 throw new Error( 977 "The property isn't a native function and will execute code in the debuggee" 978 ); 979 } 980 981 // Call the property. 982 const result = value.call(object, ...args); 983 if (result === null) { 984 throw new Error("Code was terminated."); 985 } 986 if ("throw" in result) { 987 throw result.throw; 988 } 989 return result.return; 990 } 991 992 exports.callPropertyOnObject = callPropertyOnObject; 993 994 // Convert a Debugger.Object wrapping an iterator into an iterator in the 995 // debugger's realm. 996 function* makeDebuggeeIterator(object) { 997 while (true) { 998 const nextValue = callPropertyOnObject(object, "next"); 999 if (exports.getProperty(nextValue, "done")) { 1000 break; 1001 } 1002 yield exports.getProperty(nextValue, "value"); 1003 } 1004 } 1005 1006 exports.makeDebuggeeIterator = makeDebuggeeIterator; 1007 1008 /** 1009 * Shared helper to retrieve the topmost window. This can be used to retrieve the chrome 1010 * window embedding the DevTools frame. 1011 */ 1012 function getTopWindow(win) { 1013 return win.windowRoot ? win.windowRoot.ownerGlobal : win.top; 1014 } 1015 1016 exports.getTopWindow = getTopWindow; 1017 1018 /** 1019 * Check whether two objects are identical by performing 1020 * a deep equality check on their properties and values. 1021 * See toolkit/modules/ObjectUtils.jsm for implementation. 1022 * 1023 * @param {object} a 1024 * @param {object} b 1025 * @return {boolean} 1026 */ 1027 exports.deepEqual = (a, b) => { 1028 return lazy.ObjectUtils.deepEqual(a, b); 1029 }; 1030 1031 function isWorkerDebuggerAlive(dbg) { 1032 // Some workers are zombies. `isClosed` is false, but nothing works. 1033 // `postMessage` is a noop, `addListener`'s `onClosed` doesn't work. 1034 // (Ignore dbg without `window` as they aren't related to docShell 1035 // and probably do not suffer form this issue) 1036 return !dbg.isClosed && (!dbg.window || dbg.window.docShell); 1037 } 1038 exports.isWorkerDebuggerAlive = isWorkerDebuggerAlive;