123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673 |
- /* 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;
|