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/. */
5 /* globals setImmediate, rpc */
9 /* General utilities used throughout devtools. */
11 var { Ci, Cc, Cu, components } = require("chrome");
12 var Services = require("Services");
13 var flags = require("devtools/shared/flags");
16 callFunctionWithAsyncStack,
17 } = require("devtools/shared/platform/stack");
19 loader.lazyRequireGetter(
22 "resource://gre/modules/FileUtils.jsm",
26 loader.lazyRequireGetter(
29 "resource://gre/modules/ObjectUtils.jsm",
33 // Using this name lets the eslint plugin know about lazy defines in
35 var DevToolsUtils = exports;
37 // Re-export the thread-safe utils.
38 const ThreadSafeDevToolsUtils = require("devtools/shared/ThreadSafeDevToolsUtils.js");
39 for (const key of Object.keys(ThreadSafeDevToolsUtils)) {
40 exports[key] = ThreadSafeDevToolsUtils[key];
44 * Waits for the next tick in the event loop to execute a callback.
46 exports.executeSoon = function(fn) {
51 // Only enable async stack reporting when DEBUG_JS_MODULES is set
52 // (customized local builds) to avoid a performance penalty.
53 if (AppConstants.DEBUG_JS_MODULES || flags.testing) {
54 const stack = getStack();
56 callFunctionWithAsyncStack(fn, stack, "DevToolsUtils.executeSoon");
61 Services.tm.dispatchToMainThread({
62 run: exports.makeInfallible(executor),
68 * Waits for the next tick in the event loop.
71 * A promise that is resolved after the next tick in the event loop.
73 exports.waitForTick = function() {
74 return new Promise(resolve => {
75 exports.executeSoon(resolve);
80 * Waits for the specified amount of time to pass.
83 * The amount of time to wait, in milliseconds.
85 * A promise that is resolved after the specified amount of time passes.
87 exports.waitForTime = function(delay) {
88 return new Promise(resolve => setTimeout(resolve, delay));
92 * Like XPCOMUtils.defineLazyGetter, but with a |this| sensitive getter that
93 * allows the lazy getter to be defined on a prototype and work correctly with
96 * @param Object object
97 * The prototype object to define the lazy getter on.
99 * The key to define the lazy getter on.
100 * @param Function callback
101 * The callback that will be called to determine the value. Will be
102 * called with the |this| value of the current instance.
104 exports.defineLazyPrototypeGetter = function(object, key, callback) {
105 Object.defineProperty(object, key, {
108 const value = callback.call(this);
110 Object.defineProperty(this, key, {
122 * Safely get the property value from a Debugger.Object for a given key. Walks
123 * the prototype chain until the property is found.
125 * @param {Debugger.Object} object
126 * The Debugger.Object to get the value from.
127 * @param {String} key
128 * The key to look for.
129 * @param {Boolean} invokeUnsafeGetter (defaults to false).
130 * Optional boolean to indicate if the function should execute unsafe getter
131 * in order to retrieve its result's properties.
132 * ⚠️ This should be set to true *ONLY* on user action as it may cause side-effects
133 * in the content page ⚠️
136 exports.getProperty = function(object, key, invokeUnsafeGetters = false) {
138 while (object && exports.isSafeDebuggerObject(object)) {
141 desc = object.getOwnPropertyDescriptor(key);
143 // The above can throw when the debuggee does not subsume the object's
144 // compartment, or for some WrappedNatives like Cu.Sandbox.
148 if ("value" in desc) {
151 // Call the getter if it's safe.
152 if (exports.hasSafeGetter(desc) || invokeUnsafeGetters === true) {
154 return desc.get.call(root).return;
156 // If anything goes wrong report the error and return undefined.
157 exports.reportException("getProperty", e);
162 object = object.proto;
168 * Removes all the non-opaque security wrappers of a debuggee object.
170 * @param obj Debugger.Object
171 * The debuggee object to be unwrapped.
172 * @return Debugger.Object|null|undefined
173 * - If the object has no wrapper, the same `obj` is returned. Note DeadObject
174 * objects belong to this case.
175 * - Otherwise, if the debuggee doesn't subsume object's compartment, returns `null`.
176 * - Otherwise, if the object belongs to an invisible-to-debugger compartment,
177 * returns `undefined`.
178 * - Otherwise, returns the unwrapped object.
180 exports.unwrap = function unwrap(obj) {
181 // Check if `obj` has an opaque wrapper.
182 if (obj.class === "Opaque") {
186 // Attempt to unwrap via `obj.unwrap()`. Note that:
187 // - This will return `null` if the debuggee does not subsume object's compartment.
188 // - This will throw if the object belongs to an invisible-to-debugger compartment.
189 // - This will return `obj` if there is no wrapper.
192 unwrapped = obj.unwrap();
197 // Check if further unwrapping is not possible.
198 if (!unwrapped || unwrapped === obj) {
202 // Recursively remove additional security wrappers.
203 return unwrap(unwrapped);
207 * Checks whether a debuggee object is safe. Unsafe objects may run proxy traps or throw
208 * when using `proto`, `isExtensible`, `isFrozen` or `isSealed`. Note that safe objects
209 * may still throw when calling `getOwnPropertyNames`, `getOwnPropertyDescriptor`, etc.
210 * Also note DeadObject objects are considered safe.
212 * @param obj Debugger.Object
213 * The debuggee object to be checked.
216 exports.isSafeDebuggerObject = function(obj) {
217 const unwrapped = exports.unwrap(obj);
219 // Objects belonging to an invisible-to-debugger compartment might be proxies,
220 // so just in case consider them unsafe.
221 if (unwrapped === undefined) {
225 // If the debuggee does not subsume the object's compartment, most properties won't
226 // be accessible. Cross-origin Window and Location objects might expose some, though.
227 // Therefore, it must be considered safe. Note that proxy objects have fully opaque
228 // security wrappers, so proxy traps won't run in this case.
229 if (unwrapped === null) {
233 // Proxy objects can run traps when accessed. `isProxy` getter is called on `unwrapped`
234 // instead of on `obj` in order to detect proxies behind transparent wrappers.
235 if (unwrapped.isProxy) {
243 * Determines if a descriptor has a getter which doesn't call into JavaScript.
246 * The descriptor to check for a safe getter.
248 * Whether a safe getter was found.
250 exports.hasSafeGetter = function(desc) {
251 // Scripted functions that are CCWs will not appear scripted until after
254 fn = fn && exports.unwrap(fn);
255 return fn?.callable && fn?.class == "Function" && fn?.script === undefined;
259 * Check that the property value from a Debugger.Object for a given key is an unsafe
260 * getter or not. Walks the prototype chain until the property is found.
262 * @param {Debugger.Object} object
263 * The Debugger.Object to check on.
264 * @param {String} key
265 * The key to look for.
266 * @param {Boolean} invokeUnsafeGetter (defaults to false).
267 * Optional boolean to indicate if the function should execute unsafe getter
268 * in order to retrieve its result's properties.
271 exports.isUnsafeGetter = function(object, key) {
272 while (object && exports.isSafeDebuggerObject(object)) {
275 desc = object.getOwnPropertyDescriptor(key);
277 // The above can throw when the debuggee does not subsume the object's
278 // compartment, or for some WrappedNatives like Cu.Sandbox.
282 if (Object.getOwnPropertyNames(desc).includes("get")) {
283 return !exports.hasSafeGetter(desc);
286 object = object.proto;
293 * Check if it is safe to read properties and execute methods from the given JS
294 * object. Safety is defined as being protected from unintended code execution
295 * from content scripts (or cross-compartment code).
297 * See bugs 945920 and 946752 for discussion.
300 * The object to check.
302 * True if it is safe to read properties from obj, or false otherwise.
304 exports.isSafeJSObject = function(obj) {
305 // If we are running on a worker thread, Cu is not available. In this case,
306 // we always return false, just to be on the safe side.
312 Cu.getGlobalForObject(obj) == Cu.getGlobalForObject(exports.isSafeJSObject)
314 // obj is not a cross-compartment wrapper.
318 // Xray wrappers protect against unintended code execution.
319 if (Cu.isXrayWrapper(obj)) {
323 // If there aren't Xrays, only allow chrome objects.
324 const principal = Cu.getObjectPrincipal(obj);
325 if (!principal.isSystemPrincipal) {
329 // Scripted proxy objects without Xrays can run their proxy traps.
330 if (Cu.isProxy(obj)) {
334 // Even if `obj` looks safe, an unsafe object in its prototype chain may still
335 // run unintended code, e.g. when using the `instanceof` operator.
336 const proto = Object.getPrototypeOf(obj);
337 if (proto && !exports.isSafeJSObject(proto)) {
341 // Allow non-problematic chrome objects.
346 * Dump with newline - This is a logging function that will only output when
347 * the preference "devtools.debugger.log" is set to true. Typically it is used
348 * for logging the remote debugging protocol calls.
350 exports.dumpn = function(str) {
351 if (flags.wantLogging) {
352 dump("DBG-SERVER: " + str + "\n");
357 * Dump verbose - This is a verbose logger for low-level tracing, that is typically
358 * used to provide information about the remote debugging protocol's transport
359 * mechanisms. The logging can be enabled by changing the preferences
360 * "devtools.debugger.log" and "devtools.debugger.log.verbose" to true.
362 exports.dumpv = function(msg) {
363 if (flags.wantVerbose) {
369 * Defines a getter on a specified object that will be created upon first use.
372 * The object to define the lazy getter on.
374 * The name of the getter to define on object.
376 * A function that returns what the getter should return. This will
377 * only ever be called once.
379 exports.defineLazyGetter = function(object, name, lambda) {
380 Object.defineProperty(object, name, {
383 object[name] = lambda.apply(object);
391 DevToolsUtils.defineLazyGetter(this, "AppConstants", () => {
395 return require("resource://gre/modules/AppConstants.jsm").AppConstants;
399 * No operation. The empty function.
401 exports.noop = function() {};
403 let assertionFailureCount = 0;
405 Object.defineProperty(exports, "assertionFailureCount", {
407 return assertionFailureCount;
411 function reallyAssert(condition, message) {
413 assertionFailureCount++;
414 const err = new Error("Assertion failure: " + message);
415 exports.reportException("DevToolsUtils.assert", err);
421 * DevToolsUtils.assert(condition, message)
423 * @param Boolean condition
424 * @param String message
426 * Assertions are enabled when any of the following are true:
427 * - This is a DEBUG_JS_MODULES build
428 * - flags.testing is set to true
430 * If assertions are enabled, then `condition` is checked and if false-y, the
431 * assertion failure is logged and then an error is thrown.
433 * If assertions are not enabled, then this function is a no-op.
435 Object.defineProperty(exports, "assert", {
437 AppConstants.DEBUG_JS_MODULES || flags.testing
443 * Defines a getter on a specified object for a module. The module will not
444 * be imported until first use.
447 * The object to define the lazy getter on.
449 * The name of the getter to define on object for the module.
451 * The URL used to obtain the module.
453 * The name of the symbol exported by the module.
454 * This parameter is optional and defaults to name.
456 exports.defineLazyModuleGetter = function(object, name, resource, symbol) {
457 this.defineLazyGetter(object, name, function() {
458 const temp = ChromeUtils.import(resource);
459 return temp[symbol || name];
463 DevToolsUtils.defineLazyGetter(this, "NetUtil", () => {
464 return require("resource://gre/modules/NetUtil.jsm").NetUtil;
467 DevToolsUtils.defineLazyGetter(this, "OS", () => {
468 return require("resource://gre/modules/osfile.jsm").OS;
471 DevToolsUtils.defineLazyGetter(this, "NetworkHelper", () => {
472 return require("devtools/shared/webconsole/network-helper");
476 * Performs a request to load the desired URL and returns a promise.
478 * @param urlIn String
479 * The URL we will request.
480 * @param aOptions Object
481 * An object with the following optional properties:
482 * - loadFromCache: if false, will bypass the cache and
483 * always load fresh from the network (default: true)
484 * - policy: the nsIContentPolicy type to apply when fetching the URL
485 * (only works when loading from system principal)
486 * - window: the window to get the loadGroup from
487 * - charset: the charset to use if the channel doesn't provide one
488 * - principal: the principal to use, if omitted, the request is loaded
489 * with a content principal corresponding to the url being
490 * loaded, using the origin attributes of the window, if any.
491 * - cacheKey: when loading from cache, use this key to retrieve a cache
492 * specific to a given SHEntry. (Allows loading POST
493 * requests from cache)
494 * @returns Promise that resolves with an object with the following members on
496 * - content: the document at that URL, as a string,
497 * - contentType: the content type of the document
499 * If an error occurs, the promise is rejected with that error.
501 * XXX: It may be better to use nsITraceableChannel to get to the sources
502 * without relying on caching when we can (not for eval, etc.):
503 * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
505 function mainThreadFetch(
509 policy: Ci.nsIContentPolicy.TYPE_OTHER,
516 return new Promise((resolve, reject) => {
518 const url = urlIn.split(" -> ").pop();
521 channel = newChannelForURL(url, aOptions);
527 channel.loadInfo.isInDevToolsContext = true;
529 // Set the channel options.
530 channel.loadFlags = aOptions.loadFromCache
531 ? channel.LOAD_FROM_CACHE
532 : channel.LOAD_BYPASS_CACHE;
534 if (aOptions.loadFromCache && channel instanceof Ci.nsICacheInfoChannel) {
535 // If DevTools intents to load the content from the cache,
536 // we make the LOAD_FROM_CACHE flag preferred over LOAD_BYPASS_CACHE.
537 channel.preferCacheLoadOverBypass = true;
539 // When loading from cache, the cacheKey allows us to target a specific
540 // SHEntry and offer ways to restore POST requests from cache.
541 if (aOptions.cacheKey != 0) {
542 channel.cacheKey = aOptions.cacheKey;
546 if (aOptions.window) {
547 // Respect private browsing.
548 channel.loadGroup = aOptions.window.docShell.QueryInterface(
553 // eslint-disable-next-line complexity
554 const onResponse = (stream, status, request) => {
555 if (!components.isSuccessCode(status)) {
556 reject(new Error(`Failed to fetch ${url}. Code ${status}.`));
561 // We cannot use NetUtil to do the charset conversion as if charset
562 // information is not available and our default guess is wrong the method
563 // might fail and we lose the stream data. This means we can't fall back
564 // to using the locale default encoding (bug 1181345).
566 // Read and decode the data according to the locale default encoding.
567 const available = stream.available();
568 let source = NetUtil.readInputStreamToString(stream, available);
571 // We do our own BOM sniffing here because there's no convenient
572 // implementation of the "decode" algorithm
573 // (https://encoding.spec.whatwg.org/#decode) exposed to JS.
574 let bomCharset = null;
577 source.codePointAt(0) == 0xef &&
578 source.codePointAt(1) == 0xbb &&
579 source.codePointAt(2) == 0xbf
581 bomCharset = "UTF-8";
582 source = source.slice(3);
585 source.codePointAt(0) == 0xfe &&
586 source.codePointAt(1) == 0xff
588 bomCharset = "UTF-16BE";
589 source = source.slice(2);
592 source.codePointAt(0) == 0xff &&
593 source.codePointAt(1) == 0xfe
595 bomCharset = "UTF-16LE";
596 source = source.slice(2);
599 // If the channel or the caller has correct charset information, the
600 // content will be decoded correctly. If we have to fall back to UTF-8 and
601 // the guess is wrong, the conversion fails and convertToUnicode returns
602 // the input unmodified. Essentially we try to decode the data as UTF-8
603 // and if that fails, we use the locale specific default encoding. This is
604 // the best we can do if the source does not provide charset info.
605 let charset = bomCharset;
608 charset = channel.contentCharset;
610 // Accessing `contentCharset` on content served by a service worker in
611 // non-e10s may throw.
615 charset = aOptions.charset || "UTF-8";
617 const unicodeSource = NetworkHelper.convertToUnicode(source, charset);
619 // Look for any source map URL in the response.
621 if (request instanceof Ci.nsIHttpChannel) {
623 sourceMapURL = request.getResponseHeader("SourceMap");
627 sourceMapURL = request.getResponseHeader("X-SourceMap");
633 content: unicodeSource,
634 contentType: request.contentType,
638 const uri = request.originalURI;
640 ex.name === "NS_BASE_STREAM_CLOSED" &&
641 uri instanceof Ci.nsIFileURL
643 // Empty files cause NS_BASE_STREAM_CLOSED exception. Use OS.File to
644 // differentiate between empty files and other errors (bug 1170864).
645 // This can be removed when bug 982654 is fixed.
647 uri.QueryInterface(Ci.nsIFileURL);
648 const result = OS.File.read(uri.file.path).then(bytes => {
649 // Convert the bytearray to a String.
650 const decoder = new TextDecoder();
651 const content = decoder.decode(bytes);
653 // We can't detect the contentType without opening a channel
654 // and that failed already. This is the best we can do here.
657 contentType: "text/plain",
670 NetUtil.asyncFetch(channel, onResponse);
678 * Opens a channel for given URL. Tries a bit harder than NetUtil.newChannel.
680 * @param {String} url - The URL to open a channel for.
681 * @param {Object} options - The options object passed to @method fetch.
682 * @return {nsIChannel} - The newly created channel. Throws on failure.
684 function newChannelForURL(
686 { policy, window, principal },
689 const securityFlags =
690 Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL;
694 uri = Services.io.newURI(url);
696 // In the xpcshell tests, the script url is the absolute path of the test
697 // file, which will make a malformed URI error be thrown. Add the file
698 // scheme to see if it helps.
699 uri = Services.io.newURI("file://" + url);
701 const channelOptions = {
702 contentPolicyType: policy,
703 securityFlags: securityFlags,
707 // Ensure that we have some contentPolicyType type set if one was
709 if (!channelOptions.contentPolicyType) {
710 channelOptions.contentPolicyType = Ci.nsIContentPolicy.TYPE_OTHER;
713 // If a window is provided, always use it's document as the loadingNode.
714 // This will provide the correct principal, origin attributes, service
715 // worker controller, etc.
717 channelOptions.loadingNode = window.document;
719 // If a window is not provided, then we must set a loading principal.
721 // If the caller did not provide a principal, then we use the URI
722 // to create one. Note, it's not clear what use cases require this
723 // and it may not be correct.
724 let prin = principal;
726 prin = Services.scriptSecurityManager.createContentPrincipal(uri, {});
729 channelOptions.loadingPrincipal = prin;
733 return NetUtil.newChannel(channelOptions);
735 // Don't infinitely recurse if newChannel keeps throwing.
740 // In xpcshell tests on Windows, nsExternalProtocolHandler::NewChannel()
741 // can throw NS_ERROR_UNKNOWN_PROTOCOL if the external protocol isn't
742 // supported by Windows, so we also need to handle the exception here if
743 // parsing the URL above doesn't throw.
744 return newChannelForURL(
746 { policy, window, principal },
752 // Fetch is defined differently depending on whether we are on the main thread
753 // or a worker thread.
755 // Services is not available in worker threads, nor is there any other way
756 // to fetch a URL. We need to enlist the help from the main thread here, by
757 // issuing an rpc request, to fetch the URL on our behalf.
758 exports.fetch = function(url, options) {
759 return rpc("fetch", url, options);
762 exports.fetch = mainThreadFetch;
766 * Open the file at the given path for reading.
768 * @param {String} filePath
770 * @returns Promise<nsIInputStream>
772 exports.openFileStream = function(filePath) {
773 return new Promise((resolve, reject) => {
774 const uri = NetUtil.newURI(new FileUtils.File(filePath));
776 { uri, loadUsingSystemPrincipal: true },
777 (stream, result) => {
778 if (!components.isSuccessCode(result)) {
779 reject(new Error(`Could not open "${filePath}": result = ${result}`));
790 * Save the given data to disk after asking the user where to do so.
792 * @param {Window} parentWindow
793 * The parent window to use to display the filepicker.
794 * @param {UInt8Array} dataArray
795 * The data to write to the file.
796 * @param {String} fileName
797 * The suggested filename.
798 * @param {Array} filters
799 * An array of object of the following shape:
800 * - pattern: A pattern for accepted files (example: "*.js")
801 * - label: The label that will be displayed in the save file dialog.
803 exports.saveAs = async function(
811 returnFile = await exports.showSaveFileDialog(
820 await OS.File.writeAtomic(returnFile.path, dataArray, {
821 tmpPath: returnFile.path + ".tmp",
826 * Show file picker and return the file user selected.
828 * @param {nsIWindow} parentWindow
829 * Optional parent window. If null the parent window of the file picker
830 * will be the window of the attached input element.
831 * @param {String} suggestedFilename
832 * The suggested filename.
833 * @param {Array} filters
834 * An array of object of the following shape:
835 * - pattern: A pattern for accepted files (example: "*.js")
836 * - label: The label that will be displayed in the save file dialog.
838 * A promise that is resolved after the file is selected by the file picker
840 exports.showSaveFileDialog = function(
845 const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
847 if (suggestedFilename) {
848 fp.defaultString = suggestedFilename;
851 fp.init(parentWindow, null, fp.modeSave);
852 if (Array.isArray(filters) && filters.length > 0) {
853 for (const { pattern, label } of filters) {
854 fp.appendFilter(label, pattern);
857 fp.appendFilters(fp.filterAll);
860 return new Promise((resolve, reject) => {
862 if (result == Ci.nsIFilePicker.returnCancel) {
872 * All of the flags have been moved to a different module. Make sure
873 * nobody is accessing them anymore, and don't write new code using
874 * them. We can remove this code after a while.
876 function errorOnFlag(exports, name) {
877 Object.defineProperty(exports, name, {
880 `Cannot get the flag ${name}. ` +
881 `Use the "devtools/shared/flags" module instead`;
883 throw new Error(msg);
887 `Cannot set the flag ${name}. ` +
888 `Use the "devtools/shared/flags" module instead`;
890 throw new Error(msg);
895 errorOnFlag(exports, "testing");
896 errorOnFlag(exports, "wantLogging");
897 errorOnFlag(exports, "wantVerbose");
899 // Calls the property with the given `name` on the given `object`, where
900 // `name` is a string, and `object` a Debugger.Object instance.
902 // This function uses only the Debugger.Object API to call the property. It
903 // avoids the use of unsafeDeference. This is useful for example in workers,
904 // where unsafeDereference will return an opaque security wrapper to the
906 function callPropertyOnObject(object, name, ...args) {
907 // Find the property.
911 descriptor = proto.getOwnPropertyDescriptor(name);
912 if (descriptor !== undefined) {
916 } while (proto !== null);
917 if (descriptor === undefined) {
918 throw new Error("No such property");
920 const value = descriptor.value;
921 if (typeof value !== "object" || value === null || !("callable" in value)) {
922 throw new Error("Not a callable object.");
925 // Call the property.
926 const result = value.call(object, ...args);
927 if (result === null) {
928 throw new Error("Code was terminated.");
930 if ("throw" in result) {
933 return result.return;
936 exports.callPropertyOnObject = callPropertyOnObject;
938 // Convert a Debugger.Object wrapping an iterator into an iterator in the
940 function* makeDebuggeeIterator(object) {
942 const nextValue = callPropertyOnObject(object, "next");
943 if (exports.getProperty(nextValue, "done")) {
946 yield exports.getProperty(nextValue, "value");
950 exports.makeDebuggeeIterator = makeDebuggeeIterator;
953 * Shared helper to retrieve the topmost window. This can be used to retrieve the chrome
954 * window embedding the DevTools frame.
956 function getTopWindow(win) {
957 return win.windowRoot ? win.windowRoot.ownerGlobal : win.top;
960 exports.getTopWindow = getTopWindow;
963 * Check whether two objects are identical by performing
964 * a deep equality check on their properties and values.
965 * See toolkit/modules/ObjectUtils.jsm for implementation.
971 exports.deepEqual = (a, b) => {
972 return ObjectUtils.deepEqual(a, b);
975 function isWorkerDebuggerAlive(dbg) {
976 // Some workers are zombies. `isClosed` is false, but nothing works.
977 // `postMessage` is a noop, `addListener`'s `onClosed` doesn't work.
978 // (Ignore dbg without `window` as they aren't related to docShell
979 // and probably do not suffer form this issue)
980 return !dbg.isClosed && (!dbg.window || dbg.window.docShell);
982 exports.isWorkerDebuggerAlive = isWorkerDebuggerAlive;