summaryrefslogtreecommitdiff
path: root/devtools/shared/DevToolsUtils.js
blob: d44184fd6b8e3fdd46b64fd543bbba1578a8b8af (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

/* General utilities used throughout devtools. */

var { Ci, Cu, Cc, components } = require("chrome");
var Services = require("Services");
var promise = require("promise");
var defer = require("devtools/shared/defer");
var flags = require("./flags");
var {getStack, callFunctionWithAsyncStack} = require("devtools/shared/platform/stack");

loader.lazyRequireGetter(this, "FileUtils",
                         "resource://gre/modules/FileUtils.jsm", true);

// Re-export the thread-safe utils.
const ThreadSafeDevToolsUtils = require("./ThreadSafeDevToolsUtils.js");
for (let key of Object.keys(ThreadSafeDevToolsUtils)) {
  exports[key] = ThreadSafeDevToolsUtils[key];
}

/**
 * Waits for the next tick in the event loop to execute a callback.
 */
exports.executeSoon = function executeSoon(aFn) {
  if (isWorker) {
    setImmediate(aFn);
  } else {
    let executor;
    // Only enable async stack reporting when DEBUG_JS_MODULES is set
    // (customized local builds) to avoid a performance penalty.
    if (AppConstants.DEBUG_JS_MODULES || flags.testing) {
      let stack = getStack();
      executor = () => {
        callFunctionWithAsyncStack(aFn, stack, "DevToolsUtils.executeSoon");
      };
    } else {
      executor = aFn;
    }
    Services.tm.mainThread.dispatch({
      run: exports.makeInfallible(executor)
    }, Ci.nsIThread.DISPATCH_NORMAL);
  }
};

/**
 * Waits for the next tick in the event loop.
 *
 * @return Promise
 *         A promise that is resolved after the next tick in the event loop.
 */
exports.waitForTick = function waitForTick() {
  let deferred = defer();
  exports.executeSoon(deferred.resolve);
  return deferred.promise;
};

/**
 * Waits for the specified amount of time to pass.
 *
 * @param number aDelay
 *        The amount of time to wait, in milliseconds.
 * @return Promise
 *         A promise that is resolved after the specified amount of time passes.
 */
exports.waitForTime = function waitForTime(aDelay) {
  let deferred = defer();
  setTimeout(deferred.resolve, aDelay);
  return deferred.promise;
};

/**
 * Like Array.prototype.forEach, but doesn't cause jankiness when iterating over
 * very large arrays by yielding to the browser and continuing execution on the
 * next tick.
 *
 * @param Array aArray
 *        The array being iterated over.
 * @param Function aFn
 *        The function called on each item in the array. If a promise is
 *        returned by this function, iterating over the array will be paused
 *        until the respective promise is resolved.
 * @returns Promise
 *          A promise that is resolved once the whole array has been iterated
 *          over, and all promises returned by the aFn callback are resolved.
 */
exports.yieldingEach = function yieldingEach(aArray, aFn) {
  const deferred = defer();

  let i = 0;
  let len = aArray.length;
  let outstanding = [deferred.promise];

  (function loop() {
    const start = Date.now();

    while (i < len) {
      // Don't block the main thread for longer than 16 ms at a time. To
      // maintain 60fps, you have to render every frame in at least 16ms; we
      // aren't including time spent in non-JS here, but this is Good
      // Enough(tm).
      if (Date.now() - start > 16) {
        exports.executeSoon(loop);
        return;
      }

      try {
        outstanding.push(aFn(aArray[i], i++));
      } catch (e) {
        deferred.reject(e);
        return;
      }
    }

    deferred.resolve();
  }());

  return promise.all(outstanding);
};

/**
 * Like XPCOMUtils.defineLazyGetter, but with a |this| sensitive getter that
 * allows the lazy getter to be defined on a prototype and work correctly with
 * instances.
 *
 * @param Object aObject
 *        The prototype object to define the lazy getter on.
 * @param String aKey
 *        The key to define the lazy getter on.
 * @param Function aCallback
 *        The callback that will be called to determine the value. Will be
 *        called with the |this| value of the current instance.
 */
exports.defineLazyPrototypeGetter =
function defineLazyPrototypeGetter(aObject, aKey, aCallback) {
  Object.defineProperty(aObject, aKey, {
    configurable: true,
    get: function () {
      const value = aCallback.call(this);

      Object.defineProperty(this, aKey, {
        configurable: true,
        writable: true,
        value: value
      });

      return value;
    }
  });
};

/**
 * Safely get the property value from a Debugger.Object for a given key. Walks
 * the prototype chain until the property is found.
 *
 * @param Debugger.Object aObject
 *        The Debugger.Object to get the value from.
 * @param String aKey
 *        The key to look for.
 * @return Any
 */
exports.getProperty = function getProperty(aObj, aKey) {
  let root = aObj;
  try {
    do {
      const desc = aObj.getOwnPropertyDescriptor(aKey);
      if (desc) {
        if ("value" in desc) {
          return desc.value;
        }
        // Call the getter if it's safe.
        return exports.hasSafeGetter(desc) ? desc.get.call(root).return : undefined;
      }
      aObj = aObj.proto;
    } while (aObj);
  } catch (e) {
    // If anything goes wrong report the error and return undefined.
    exports.reportException("getProperty", e);
  }
  return undefined;
};

/**
 * Determines if a descriptor has a getter which doesn't call into JavaScript.
 *
 * @param Object aDesc
 *        The descriptor to check for a safe getter.
 * @return Boolean
 *         Whether a safe getter was found.
 */
exports.hasSafeGetter = function hasSafeGetter(aDesc) {
  // Scripted functions that are CCWs will not appear scripted until after
  // unwrapping.
  try {
    let fn = aDesc.get.unwrap();
    return fn && fn.callable && fn.class == "Function" && fn.script === undefined;
  } catch (e) {
    // Avoid exception 'Object in compartment marked as invisible to Debugger'
    return false;
  }
};

/**
 * Check if it is safe to read properties and execute methods from the given JS
 * object. Safety is defined as being protected from unintended code execution
 * from content scripts (or cross-compartment code).
 *
 * See bugs 945920 and 946752 for discussion.
 *
 * @type Object aObj
 *       The object to check.
 * @return Boolean
 *         True if it is safe to read properties from aObj, or false otherwise.
 */
exports.isSafeJSObject = function isSafeJSObject(aObj) {
  // If we are running on a worker thread, Cu is not available. In this case,
  // we always return false, just to be on the safe side.
  if (isWorker) {
    return false;
  }

  if (Cu.getGlobalForObject(aObj) ==
      Cu.getGlobalForObject(exports.isSafeJSObject)) {
    return true; // aObj is not a cross-compartment wrapper.
  }

  let principal = Cu.getObjectPrincipal(aObj);
  if (Services.scriptSecurityManager.isSystemPrincipal(principal)) {
    return true; // allow chrome objects
  }

  return Cu.isXrayWrapper(aObj);
};

exports.dumpn = function dumpn(str) {
  if (flags.wantLogging) {
    dump("DBG-SERVER: " + str + "\n");
  }
};

/**
 * A verbose logger for low-level tracing.
 */
exports.dumpv = function (msg) {
  if (flags.wantVerbose) {
    exports.dumpn(msg);
  }
};

/**
 * Defines a getter on a specified object that will be created upon first use.
 *
 * @param aObject
 *        The object to define the lazy getter on.
 * @param aName
 *        The name of the getter to define on aObject.
 * @param aLambda
 *        A function that returns what the getter should return.  This will
 *        only ever be called once.
 */
exports.defineLazyGetter = function defineLazyGetter(aObject, aName, aLambda) {
  Object.defineProperty(aObject, aName, {
    get: function () {
      delete aObject[aName];
      return aObject[aName] = aLambda.apply(aObject);
    },
    configurable: true,
    enumerable: true
  });
};

exports.defineLazyGetter(this, "AppConstants", () => {
  if (isWorker) {
    return {};
  }
  const scope = {};
  Cu.import("resource://gre/modules/AppConstants.jsm", scope);
  return scope.AppConstants;
});

/**
 * No operation. The empty function.
 */
exports.noop = function () { };

let assertionFailureCount = 0;

Object.defineProperty(exports, "assertionFailureCount", {
  get() {
    return assertionFailureCount;
  }
});

function reallyAssert(condition, message) {
  if (!condition) {
    assertionFailureCount++;
    const err = new Error("Assertion failure: " + message);
    exports.reportException("DevToolsUtils.assert", err);
    throw err;
  }
}

/**
 * DevToolsUtils.assert(condition, message)
 *
 * @param Boolean condition
 * @param String message
 *
 * Assertions are enabled when any of the following are true:
 *   - This is a DEBUG_JS_MODULES build
 *   - This is a DEBUG build
 *   - flags.testing is set to true
 *
 * If assertions are enabled, then `condition` is checked and if false-y, the
 * assertion failure is logged and then an error is thrown.
 *
 * If assertions are not enabled, then this function is a no-op.
 */
Object.defineProperty(exports, "assert", {
  get: () => (AppConstants.DEBUG || AppConstants.DEBUG_JS_MODULES || flags.testing)
    ? reallyAssert
    : exports.noop,
});

/**
 * Defines a getter on a specified object for a module.  The module will not
 * be imported until first use.
 *
 * @param aObject
 *        The object to define the lazy getter on.
 * @param aName
 *        The name of the getter to define on aObject for the module.
 * @param aResource
 *        The URL used to obtain the module.
 * @param aSymbol
 *        The name of the symbol exported by the module.
 *        This parameter is optional and defaults to aName.
 */
exports.defineLazyModuleGetter = function defineLazyModuleGetter(aObject, aName,
                                                                 aResource,
                                                                 aSymbol)
{
  this.defineLazyGetter(aObject, aName, function XPCU_moduleLambda() {
    var temp = {};
    Cu.import(aResource, temp);
    return temp[aSymbol || aName];
  });
};

exports.defineLazyGetter(this, "NetUtil", () => {
  return Cu.import("resource://gre/modules/NetUtil.jsm", {}).NetUtil;
});

exports.defineLazyGetter(this, "OS", () => {
  return Cu.import("resource://gre/modules/osfile.jsm", {}).OS;
});

exports.defineLazyGetter(this, "TextDecoder", () => {
  return Cu.import("resource://gre/modules/osfile.jsm", {}).TextDecoder;
});

exports.defineLazyGetter(this, "NetworkHelper", () => {
  return require("devtools/shared/webconsole/network-helper");
});

/**
 * Performs a request to load the desired URL and returns a promise.
 *
 * @param aURL String
 *        The URL we will request.
 * @param aOptions Object
 *        An object with the following optional properties:
 *        - loadFromCache: if false, will bypass the cache and
 *          always load fresh from the network (default: true)
 *        - policy: the nsIContentPolicy type to apply when fetching the URL
 *                  (only works when loading from system principal)
 *        - window: the window to get the loadGroup from
 *        - charset: the charset to use if the channel doesn't provide one
 *        - principal: the principal to use, if omitted, the request is loaded
 *                     with a codebase principal corresponding to the url being
 *                     loaded, using the origin attributes of the window, if any.
 *        - cacheKey: when loading from cache, use this key to retrieve a cache
 *                    specific to a given SHEntry. (Allows loading POST
 *                    requests from cache)
 * @returns Promise that resolves with an object with the following members on
 *          success:
 *           - content: the document at that URL, as a string,
 *           - contentType: the content type of the document
 *
 *          If an error occurs, the promise is rejected with that error.
 *
 * XXX: It may be better to use nsITraceableChannel to get to the sources
 * without relying on caching when we can (not for eval, etc.):
 * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
 */
function mainThreadFetch(aURL, aOptions = { loadFromCache: true,
                                          policy: Ci.nsIContentPolicy.TYPE_OTHER,
                                          window: null,
                                          charset: null,
                                          principal: null,
                                          cacheKey: null }) {
  // Create a channel.
  let url = aURL.split(" -> ").pop();
  let channel;
  try {
    channel = newChannelForURL(url, aOptions);
  } catch (ex) {
    return promise.reject(ex);
  }

  // Set the channel options.
  channel.loadFlags = aOptions.loadFromCache
    ? channel.LOAD_FROM_CACHE
    : channel.LOAD_BYPASS_CACHE;

  // When loading from cache, the cacheKey allows us to target a specific
  // SHEntry and offer ways to restore POST requests from cache.
  if (aOptions.loadFromCache &&
      aOptions.cacheKey && channel instanceof Ci.nsICacheInfoChannel) {
    channel.cacheKey = aOptions.cacheKey;
  }

  if (aOptions.window) {
    // Respect private browsing.
    channel.loadGroup = aOptions.window.QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIWebNavigation)
                          .QueryInterface(Ci.nsIDocumentLoader)
                          .loadGroup;
  }

  let deferred = defer();
  let onResponse = (stream, status, request) => {
    if (!components.isSuccessCode(status)) {
      deferred.reject(new Error(`Failed to fetch ${url}. Code ${status}.`));
      return;
    }

    try {
      // We cannot use NetUtil to do the charset conversion as if charset
      // information is not available and our default guess is wrong the method
      // might fail and we lose the stream data. This means we can't fall back
      // to using the locale default encoding (bug 1181345).

      // Read and decode the data according to the locale default encoding.
      let available = stream.available();
      let source = NetUtil.readInputStreamToString(stream, available);
      stream.close();

      // We do our own BOM sniffing here because there's no convenient
      // implementation of the "decode" algorithm
      // (https://encoding.spec.whatwg.org/#decode) exposed to JS.
      let bomCharset = null;
      if (available >= 3 && source.codePointAt(0) == 0xef &&
          source.codePointAt(1) == 0xbb && source.codePointAt(2) == 0xbf) {
        bomCharset = "UTF-8";
        source = source.slice(3);
      } else if (available >= 2 && source.codePointAt(0) == 0xfe &&
                 source.codePointAt(1) == 0xff) {
        bomCharset = "UTF-16BE";
        source = source.slice(2);
      } else if (available >= 2 && source.codePointAt(0) == 0xff &&
                 source.codePointAt(1) == 0xfe) {
        bomCharset = "UTF-16LE";
        source = source.slice(2);
      }

      // If the channel or the caller has correct charset information, the
      // content will be decoded correctly. If we have to fall back to UTF-8 and
      // the guess is wrong, the conversion fails and convertToUnicode returns
      // the input unmodified. Essentially we try to decode the data as UTF-8
      // and if that fails, we use the locale specific default encoding. This is
      // the best we can do if the source does not provide charset info.
      let charset = bomCharset || channel.contentCharset || aOptions.charset || "UTF-8";
      let unicodeSource = NetworkHelper.convertToUnicode(source, charset);

      deferred.resolve({
        content: unicodeSource,
        contentType: request.contentType
      });
    } catch (ex) {
      let uri = request.originalURI;
      if (ex.name === "NS_BASE_STREAM_CLOSED" && uri instanceof Ci.nsIFileURL) {
        // Empty files cause NS_BASE_STREAM_CLOSED exception. Use OS.File to
        // differentiate between empty files and other errors (bug 1170864).
        // This can be removed when bug 982654 is fixed.

        uri.QueryInterface(Ci.nsIFileURL);
        let result = OS.File.read(uri.file.path).then(bytes => {
          // Convert the bytearray to a String.
          let decoder = new TextDecoder();
          let content = decoder.decode(bytes);

          // We can't detect the contentType without opening a channel
          // and that failed already. This is the best we can do here.
          return {
            content,
            contentType: "text/plain"
          };
        });

        deferred.resolve(result);
      } else {
        deferred.reject(ex);
      }
    }
  };

  // Open the channel
  try {
    NetUtil.asyncFetch(channel, onResponse);
  } catch (ex) {
    return promise.reject(ex);
  }

  return deferred.promise;
}

/**
 * Opens a channel for given URL. Tries a bit harder than NetUtil.newChannel.
 *
 * @param {String} url - The URL to open a channel for.
 * @param {Object} options - The options object passed to @method fetch.
 * @return {nsIChannel} - The newly created channel. Throws on failure.
 */
function newChannelForURL(url, { policy, window, principal }) {
  var securityFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL;

  let uri;
  try {
    uri = Services.io.newURI(url, null, null);
  } catch (e) {
    // In the xpcshell tests, the script url is the absolute path of the test
    // file, which will make a malformed URI error be thrown. Add the file
    // scheme to see if it helps.
    uri = Services.io.newURI("file://" + url, null, null);
  }
  let channelOptions = {
    contentPolicyType: policy,
    securityFlags: securityFlags,
    uri: uri
  };
  let prin = principal;
  if (!prin) {
    let oa = {};
    if (window) {
      oa = window.document.nodePrincipal.originAttributes;
    }
    prin = Services.scriptSecurityManager
                   .createCodebasePrincipal(uri, oa);
  }
  // contentPolicyType is required when specifying a principal
  if (!channelOptions.contentPolicyType) {
    channelOptions.contentPolicyType = Ci.nsIContentPolicy.TYPE_OTHER;
  }
  channelOptions.loadingPrincipal = prin;

  try {
    return NetUtil.newChannel(channelOptions);
  } catch (e) {
    // In xpcshell tests on Windows, nsExternalProtocolHandler::NewChannel()
    // can throw NS_ERROR_UNKNOWN_PROTOCOL if the external protocol isn't
    // supported by Windows, so we also need to handle the exception here if
    // parsing the URL above doesn't throw.
    return newChannelForURL("file://" + url, { policy, window, principal });
  }
}

// Fetch is defined differently depending on whether we are on the main thread
// or a worker thread.
if (!this.isWorker) {
  exports.fetch = mainThreadFetch;
} else {
  // Services is not available in worker threads, nor is there any other way
  // to fetch a URL. We need to enlist the help from the main thread here, by
  // issuing an rpc request, to fetch the URL on our behalf.
  exports.fetch = function (url, options) {
    return rpc("fetch", url, options);
  };
}

/**
 * Open the file at the given path for reading.
 *
 * @param {String} filePath
 *
 * @returns Promise<nsIInputStream>
 */
exports.openFileStream = function (filePath) {
  return new Promise((resolve, reject) => {
    const uri = NetUtil.newURI(new FileUtils.File(filePath));
    NetUtil.asyncFetch(
      { uri, loadUsingSystemPrincipal: true },
      (stream, result) => {
        if (!components.isSuccessCode(result)) {
          reject(new Error(`Could not open "${filePath}": result = ${result}`));
          return;
        }

        resolve(stream);
      }
    );
  });
};

/*
 * All of the flags have been moved to a different module. Make sure
 * nobody is accessing them anymore, and don't write new code using
 * them. We can remove this code after a while.
 */
function errorOnFlag(exports, name) {
  Object.defineProperty(exports, name, {
    get: () => {
      const msg = `Cannot get the flag ${name}. ` +
            `Use the "devtools/shared/flags" module instead`;
      console.error(msg);
      throw new Error(msg);
    },
    set: () => {
      const msg = `Cannot set the flag ${name}. ` +
            `Use the "devtools/shared/flags" module instead`;
      console.error(msg);
      throw new Error(msg);
    }
  });
}

errorOnFlag(exports, "testing");
errorOnFlag(exports, "wantLogging");
errorOnFlag(exports, "wantVerbose");

// Calls the property with the given `name` on the given `object`, where
// `name` is a string, and `object` a Debugger.Object instance.
///
// This function uses only the Debugger.Object API to call the property. It
// avoids the use of unsafeDeference. This is useful for example in workers,
// where unsafeDereference will return an opaque security wrapper to the
// referent.
function callPropertyOnObject(object, name) {
  // Find the property.
  let descriptor;
  let proto = object;
  do {
    descriptor = proto.getOwnPropertyDescriptor(name);
    if (descriptor !== undefined) {
      break;
    }
    proto = proto.proto;
  } while (proto !== null);
  if (descriptor === undefined) {
    throw new Error("No such property");
  }
  let value = descriptor.value;
  if (typeof value !== "object" || value === null || !("callable" in value)) {
    throw new Error("Not a callable object.");
  }

  // Call the property.
  let result = value.call(object);
  if (result === null) {
    throw new Error("Code was terminated.");
  }
  if ("throw" in result) {
    throw result.throw;
  }
  return result.return;
}


exports.callPropertyOnObject = callPropertyOnObject;