tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

js-property-provider.js (23861B)


      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 "use strict";
      6 
      7 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
      8 
      9 const {
     10  evalWithDebugger,
     11 } = require("resource://devtools/server/actors/webconsole/eval-with-debugger.js");
     12 
     13 if (!isWorker) {
     14  loader.lazyRequireGetter(
     15    this,
     16    "getSyntaxTrees",
     17    "resource://devtools/shared/webconsole/parser-helper.js",
     18    true
     19  );
     20 }
     21 const lazy = {};
     22 if (!isWorker) {
     23  ChromeUtils.defineESModuleGetters(
     24    lazy,
     25    {
     26      Reflect: "resource://gre/modules/reflect.sys.mjs",
     27    },
     28    { global: "contextual" }
     29  );
     30 }
     31 loader.lazyRequireGetter(
     32  this,
     33  [
     34    "analyzeInputString",
     35    "shouldInputBeAutocompleted",
     36    "shouldInputBeEagerlyEvaluated",
     37  ],
     38  "resource://devtools/shared/webconsole/analyze-input-string.js",
     39  true
     40 );
     41 
     42 // Provide an easy way to bail out of even attempting an autocompletion
     43 // if an object has way too many properties. Protects against large objects
     44 // with numeric values that wouldn't be tallied towards MAX_AUTOCOMPLETIONS.
     45 const MAX_AUTOCOMPLETE_ATTEMPTS = (exports.MAX_AUTOCOMPLETE_ATTEMPTS = 100000);
     46 // Prevent iterating over too many properties during autocomplete suggestions.
     47 const MAX_AUTOCOMPLETIONS = (exports.MAX_AUTOCOMPLETIONS = 1500);
     48 
     49 /**
     50 * Provides a list of properties, that are possible matches based on the passed
     51 * Debugger.Environment/Debugger.Object and inputValue.
     52 *
     53 * @param {object} An object of the following shape:
     54 * - {Object} dbgObject
     55 *        When the debugger is not paused this Debugger.Object wraps
     56 *        the scope for autocompletion.
     57 *        It is null if the debugger is paused.
     58 * - {Object} environment
     59 *        When the debugger is paused this Debugger.Environment is the
     60 *        scope for autocompletion.
     61 *        It is null if the debugger is not paused.
     62 * - {String} inputValue
     63 *        Value that should be completed.
     64 * - {Number} cursor (defaults to inputValue.length).
     65 *        Optional offset in the input where the cursor is located. If this is
     66 *        omitted then the cursor is assumed to be at the end of the input
     67 *        value.
     68 * - {Array} authorizedEvaluations (defaults to []).
     69 *        Optional array containing all the different properties access that the engine
     70 *        can execute in order to retrieve its result's properties.
     71 *        ⚠️ This should be set to true *ONLY* on user action as it may cause side-effects
     72 *        in the content page ⚠️
     73 * - {WebconsoleActor} webconsoleActor
     74 *        A reference to a webconsole actor which we can use to retrieve the last
     75 *        evaluation result or create a debuggee value.
     76 * - {String}: selectedNodeActor
     77 *        The actor id of the selected node in the inspector.
     78 * - {Array<string>}: expressionVars
     79 *        Optional array containing variable defined in the expression. Those variables
     80 *        are extracted from CodeMirror state.
     81 * @returns null or object
     82 *          If the inputValue is an unsafe getter and invokeUnsafeGetter is false, the
     83 *          following form is returned:
     84 *
     85 *          {
     86 *            isUnsafeGetter: true,
     87 *            getterPath: {Array<String>} An array of the property chain leading to the
     88 *                        getter. Example: ["x", "myGetter"]
     89 *          }
     90 *
     91 *          If no completion valued could be computed, and the input is not an unsafe
     92 *          getter, null is returned.
     93 *
     94 *          Otherwise an object with the following form is returned:
     95 *            {
     96 *              matches: Set<string>
     97 *              matchProp: Last part of the inputValue that was used to find
     98 *                         the matches-strings.
     99 *              isElementAccess: Boolean set to true if the evaluation is an element
    100 *                               access (e.g. `window["addEvent`).
    101 *            }
    102 */
    103 // eslint-disable-next-line complexity
    104 function jsPropertyProvider({
    105  dbgObject,
    106  environment,
    107  frameActorId,
    108  inputValue,
    109  cursor,
    110  authorizedEvaluations = [],
    111  webconsoleActor,
    112  selectedNodeActor,
    113  expressionVars = [],
    114 }) {
    115  if (cursor === undefined) {
    116    cursor = inputValue.length;
    117  }
    118 
    119  inputValue = inputValue.substring(0, cursor);
    120 
    121  // Analyse the inputValue and find the beginning of the last part that
    122  // should be completed.
    123  const inputAnalysis = analyzeInputString(inputValue);
    124 
    125  if (!shouldInputBeAutocompleted(inputAnalysis)) {
    126    return null;
    127  }
    128 
    129  let {
    130    lastStatement,
    131    isElementAccess,
    132    mainExpression,
    133    matchProp,
    134    isPropertyAccess,
    135  } = inputAnalysis;
    136 
    137  // Eagerly evaluate the main expression and return the results properties.
    138  // e.g. `obj.func().a` will evaluate `obj.func()` and return properties matching `a`.
    139  // NOTE: this is only useful when the input has a property access.
    140  if (webconsoleActor && shouldInputBeEagerlyEvaluated(inputAnalysis)) {
    141    const eagerResponse = evalWithDebugger(
    142      mainExpression,
    143      { eager: true, selectedNodeActor, frameActor: frameActorId },
    144      webconsoleActor
    145    );
    146 
    147    const ret = eagerResponse?.result?.return;
    148 
    149    // Only send matches if eager evaluation returned something meaningful
    150    if (ret && ret !== undefined) {
    151      const matches =
    152        typeof ret != "object"
    153          ? getMatchedProps(ret, matchProp)
    154          : getMatchedPropsInDbgObject(ret, matchProp);
    155 
    156      return prepareReturnedObject({
    157        matches,
    158        search: matchProp,
    159        isElementAccess,
    160      });
    161    }
    162  }
    163 
    164  // AST representation of the expression before the last access char (`.` or `[`).
    165  let astExpression;
    166  const startQuoteRegex = /^('|"|`)/;
    167  const env = environment || dbgObject.asEnvironment();
    168 
    169  // Catch literals like [1,2,3] or "foo" and return the matches from
    170  // their prototypes.
    171  // Don't run this is a worker, migrating to acorn should allow this
    172  // to run in a worker - Bug 1217198.
    173  if (!isWorker && isPropertyAccess) {
    174    const syntaxTrees = getSyntaxTrees(mainExpression);
    175    const lastTree = syntaxTrees[syntaxTrees.length - 1];
    176    const lastBody = lastTree?.body[lastTree.body.length - 1];
    177 
    178    // Finding the last expression since we've sliced up until the dot.
    179    // If there were parse errors this won't exist.
    180    if (lastBody) {
    181      if (!lastBody.expression) {
    182        return null;
    183      }
    184 
    185      astExpression = lastBody.expression;
    186      let matchingObject;
    187 
    188      if (astExpression.type === "ArrayExpression") {
    189        matchingObject = getContentPrototypeObject(env, "Array");
    190      } else if (
    191        astExpression.type === "Literal" &&
    192        typeof astExpression.value === "string"
    193      ) {
    194        matchingObject = getContentPrototypeObject(env, "String");
    195      } else if (
    196        astExpression.type === "Literal" &&
    197        Number.isFinite(astExpression.value)
    198      ) {
    199        // The parser rightfuly indicates that we have a number in some cases (e.g. `1.`),
    200        // but we don't want to return Number proto properties in that case since
    201        // the result would be invalid (i.e. `1.toFixed()` throws).
    202        // So if the expression value is an integer, it should not end with `{Number}.`
    203        // (but the following are fine: `1..`, `(1.).`).
    204        if (
    205          !Number.isInteger(astExpression.value) ||
    206          /\d[^\.]{0}\.$/.test(lastStatement) === false
    207        ) {
    208          matchingObject = getContentPrototypeObject(env, "Number");
    209        } else {
    210          return null;
    211        }
    212      }
    213 
    214      if (matchingObject) {
    215        let search = matchProp;
    216 
    217        let elementAccessQuote;
    218        if (isElementAccess && startQuoteRegex.test(matchProp)) {
    219          elementAccessQuote = matchProp[0];
    220          search = matchProp.replace(startQuoteRegex, "");
    221        }
    222 
    223        let props = getMatchedPropsInDbgObject(matchingObject, search);
    224 
    225        if (isElementAccess) {
    226          props = wrapMatchesInQuotes(props, elementAccessQuote);
    227        }
    228 
    229        return {
    230          isElementAccess,
    231          matchProp,
    232          matches: props,
    233        };
    234      }
    235    }
    236  }
    237 
    238  // We are completing a variable / a property lookup.
    239  let properties = [];
    240 
    241  if (astExpression) {
    242    if (isPropertyAccess) {
    243      properties = getPropertiesFromAstExpression(astExpression);
    244 
    245      if (properties === null) {
    246        return null;
    247      }
    248    }
    249  } else {
    250    properties = lastStatement.split(".");
    251    if (isElementAccess) {
    252      const lastPart = properties[properties.length - 1];
    253      const openBracketIndex = lastPart.lastIndexOf("[");
    254      matchProp = lastPart.substr(openBracketIndex + 1);
    255      properties[properties.length - 1] = lastPart.substring(
    256        0,
    257        openBracketIndex
    258      );
    259    } else {
    260      matchProp = properties.pop().trimLeft();
    261    }
    262  }
    263 
    264  let search = matchProp;
    265  let elementAccessQuote;
    266  if (isElementAccess && startQuoteRegex.test(search)) {
    267    elementAccessQuote = search[0];
    268    search = search.replace(startQuoteRegex, "");
    269  }
    270 
    271  let obj = dbgObject;
    272  if (properties.length === 0) {
    273    const environmentProperties = getMatchedPropsInEnvironment(env, search);
    274    const expressionVariables = new Set(
    275      expressionVars.filter(variableName => variableName.startsWith(matchProp))
    276    );
    277 
    278    for (const prop of environmentProperties) {
    279      expressionVariables.add(prop);
    280    }
    281 
    282    return {
    283      isElementAccess,
    284      matchProp,
    285      matches: expressionVariables,
    286    };
    287  }
    288 
    289  let firstProp = properties.shift();
    290  if (typeof firstProp == "string") {
    291    firstProp = firstProp.trim();
    292  }
    293 
    294  if (firstProp === "this") {
    295    // Special case for 'this' - try to get the Object from the Environment.
    296    // No problem if it throws, we will just not autocomplete.
    297    try {
    298      obj = env.object;
    299    } catch (e) {
    300      // Ignore.
    301    }
    302  } else if (firstProp === "$_" && webconsoleActor) {
    303    obj = webconsoleActor.getLastConsoleInputEvaluation();
    304  } else if (firstProp === "$0" && selectedNodeActor && webconsoleActor) {
    305    const actor = webconsoleActor.conn.getActor(selectedNodeActor);
    306    if (actor) {
    307      try {
    308        obj = webconsoleActor.makeDebuggeeValue(actor.rawNode);
    309      } catch (e) {
    310        // Ignore.
    311      }
    312    }
    313  } else if (hasArrayIndex(firstProp)) {
    314    obj = getArrayMemberProperty(null, env, firstProp);
    315  } else {
    316    obj = getVariableInEnvironment(env, firstProp);
    317  }
    318 
    319  if (!isObjectUsable(obj)) {
    320    return null;
    321  }
    322 
    323  // We get the rest of the properties recursively starting from the
    324  // Debugger.Object that wraps the first property
    325  for (let [index, prop] of properties.entries()) {
    326    if (typeof prop === "string") {
    327      prop = prop.trim();
    328    }
    329 
    330    if (prop === undefined || prop === null || prop === "") {
    331      return null;
    332    }
    333 
    334    const propPath = [firstProp].concat(properties.slice(0, index + 1));
    335    const authorized = authorizedEvaluations.some(
    336      x => JSON.stringify(x) === JSON.stringify(propPath)
    337    );
    338 
    339    if (!authorized && DevToolsUtils.isUnsafeGetter(obj, prop)) {
    340      // If we try to access an unsafe getter, return its name so we can consume that
    341      // on the frontend.
    342      return {
    343        isUnsafeGetter: true,
    344        getterPath: propPath,
    345      };
    346    }
    347 
    348    if (hasArrayIndex(prop)) {
    349      // The property to autocomplete is a member of array. For example
    350      // list[i][j]..[n]. Traverse the array to get the actual element.
    351      obj = getArrayMemberProperty(obj, null, prop);
    352    } else {
    353      obj = DevToolsUtils.getProperty(obj, prop, authorized);
    354    }
    355 
    356    if (!isObjectUsable(obj)) {
    357      return null;
    358    }
    359  }
    360 
    361  const matches =
    362    typeof obj != "object"
    363      ? getMatchedProps(obj, search)
    364      : getMatchedPropsInDbgObject(obj, search);
    365  return prepareReturnedObject({
    366    matches,
    367    search,
    368    isElementAccess,
    369    elementAccessQuote,
    370  });
    371 }
    372 
    373 function hasArrayIndex(str) {
    374  return /\[\d+\]$/.test(str);
    375 }
    376 
    377 /**
    378 * For a given environment and constructor name, returns its Debugger.Object wrapped
    379 * prototype.
    380 *
    381 * @param {Environment} env
    382 * @param {string} name: Name of the constructor object we want the prototype of.
    383 * @returns {Debugger.Object|null} the prototype, or null if it not found.
    384 */
    385 function getContentPrototypeObject(env, name) {
    386  // Retrieve the outermost environment to get the global object.
    387  let outermostEnv = env;
    388  while (outermostEnv?.parent) {
    389    outermostEnv = outermostEnv.parent;
    390  }
    391 
    392  const constructorObj = DevToolsUtils.getProperty(outermostEnv.object, name);
    393  if (!constructorObj) {
    394    return null;
    395  }
    396 
    397  return DevToolsUtils.getProperty(constructorObj, "prototype");
    398 }
    399 
    400 /**
    401 * @param {object} ast: An AST representing a property access (e.g. `foo.bar["baz"].x`)
    402 * @returns {Array|null} An array representing the property access
    403 *                       (e.g. ["foo", "bar", "baz", "x"]).
    404 */
    405 function getPropertiesFromAstExpression(ast) {
    406  let result = [];
    407  if (!ast) {
    408    return result;
    409  }
    410  const { type, property, object, name, expression } = ast;
    411  if (type === "ThisExpression") {
    412    result.unshift("this");
    413  } else if (type === "Identifier" && name) {
    414    result.unshift(name);
    415  } else if (type === "OptionalExpression" && expression) {
    416    result = (getPropertiesFromAstExpression(expression) || []).concat(result);
    417  } else if (
    418    type === "MemberExpression" ||
    419    type === "OptionalMemberExpression"
    420  ) {
    421    if (property) {
    422      if (property.type === "Identifier" && property.name) {
    423        result.unshift(property.name);
    424      } else if (property.type === "Literal") {
    425        result.unshift(property.value);
    426      }
    427    }
    428    if (object) {
    429      result = (getPropertiesFromAstExpression(object) || []).concat(result);
    430    }
    431  } else {
    432    return null;
    433  }
    434  return result;
    435 }
    436 
    437 function wrapMatchesInQuotes(matches, quote = `"`) {
    438  return new Set(
    439    [...matches].map(p => {
    440      // Escape as a double-quoted string literal
    441      p = JSON.stringify(p);
    442 
    443      // We don't have to do anything more when using double quotes
    444      if (quote == `"`) {
    445        return p;
    446      }
    447 
    448      // Remove surrounding double quotes
    449      p = p.slice(1, -1);
    450 
    451      // Unescape inner double quotes (all must be escaped, so no need to count backslashes)
    452      p = p.replace(/\\(?=")/g, "");
    453 
    454      // Escape the specified quote (assuming ' or `, which are treated literally in regex)
    455      p = p.replace(new RegExp(quote, "g"), "\\$&");
    456 
    457      // Template literals treat ${ specially, escape it
    458      if (quote == "`") {
    459        p = p.replace(/\${/g, "\\$&");
    460      }
    461 
    462      // Surround the result with quotes
    463      return `${quote}${p}${quote}`;
    464    })
    465  );
    466 }
    467 
    468 /**
    469 * Get the array member of obj for the given prop. For example, given
    470 * prop='list[0][1]' the element at [0][1] of obj.list is returned.
    471 *
    472 * @param object obj
    473 *        The object to operate on. Should be null if env is passed.
    474 * @param object env
    475 *        The Environment to operate in. Should be null if obj is passed.
    476 * @param string prop
    477 *        The property to return.
    478 * @return null or Object
    479 *         Returns null if the property couldn't be located. Otherwise the array
    480 *         member identified by prop.
    481 */
    482 function getArrayMemberProperty(obj, env, prop) {
    483  // First get the array.
    484  const propWithoutIndices = prop.substr(0, prop.indexOf("["));
    485 
    486  if (env) {
    487    obj = getVariableInEnvironment(env, propWithoutIndices);
    488  } else {
    489    obj = DevToolsUtils.getProperty(obj, propWithoutIndices);
    490  }
    491 
    492  if (!isObjectUsable(obj)) {
    493    return null;
    494  }
    495 
    496  // Then traverse the list of indices to get the actual element.
    497  let result;
    498  const arrayIndicesRegex = /\[[^\]]*\]/g;
    499  while ((result = arrayIndicesRegex.exec(prop)) !== null) {
    500    const indexWithBrackets = result[0];
    501    const indexAsText = indexWithBrackets.substr(
    502      1,
    503      indexWithBrackets.length - 2
    504    );
    505    const index = parseInt(indexAsText, 10);
    506 
    507    if (isNaN(index)) {
    508      return null;
    509    }
    510 
    511    obj = DevToolsUtils.getProperty(obj, index);
    512 
    513    if (!isObjectUsable(obj)) {
    514      return null;
    515    }
    516  }
    517 
    518  return obj;
    519 }
    520 
    521 /**
    522 * Check if the given Debugger.Object can be used for autocomplete.
    523 *
    524 * @param Debugger.Object object
    525 *        The Debugger.Object to check.
    526 * @return boolean
    527 *         True if further inspection into the object is possible, or false
    528 *         otherwise.
    529 */
    530 function isObjectUsable(object) {
    531  if (object == null) {
    532    return false;
    533  }
    534 
    535  if (typeof object == "object" && object.class == "DeadObject") {
    536    return false;
    537  }
    538 
    539  return true;
    540 }
    541 
    542 /**
    543 * @see getExactMatchImpl()
    544 */
    545 function getVariableInEnvironment(environment, name) {
    546  return getExactMatchImpl(environment, name, DebuggerEnvironmentSupport);
    547 }
    548 
    549 function prepareReturnedObject({
    550  matches,
    551  search,
    552  isElementAccess,
    553  elementAccessQuote,
    554 }) {
    555  if (isElementAccess) {
    556    // If it's an element access, we need to wrap properties in quotes (either the one
    557    // the user already typed, or `"`).
    558 
    559    matches = wrapMatchesInQuotes(matches, elementAccessQuote);
    560  } else if (!isWorker) {
    561    // If we're not performing an element access, we need to check that the property
    562    // are suited for a dot access. (reflect.sys.mjs is not available in worker context yet,
    563    // see Bug 1507181).
    564    for (const match of matches) {
    565      try {
    566        // In order to know if the property is suited for dot notation, we use Reflect
    567        // to parse an expression where we try to access the property with a dot. If it
    568        // throws, this means that we need to do an element access instead.
    569        lazy.Reflect.parse(`({${match}: true})`);
    570      } catch (e) {
    571        matches.delete(match);
    572      }
    573    }
    574  }
    575 
    576  return { isElementAccess, matchProp: search, matches };
    577 }
    578 
    579 /**
    580 * @see getMatchedPropsImpl()
    581 */
    582 function getMatchedPropsInEnvironment(environment, match) {
    583  return getMatchedPropsImpl(environment, match, DebuggerEnvironmentSupport);
    584 }
    585 
    586 /**
    587 * @see getMatchedPropsImpl()
    588 */
    589 function getMatchedPropsInDbgObject(dbgObject, match) {
    590  return getMatchedPropsImpl(dbgObject, match, DebuggerObjectSupport);
    591 }
    592 
    593 /**
    594 * @see getMatchedPropsImpl()
    595 */
    596 function getMatchedProps(obj, match) {
    597  if (typeof obj != "object") {
    598    obj = obj.constructor.prototype;
    599  }
    600  return getMatchedPropsImpl(obj, match, JSObjectSupport);
    601 }
    602 
    603 /**
    604 * Get all properties in the given object (and its parent prototype chain) that
    605 * match a given prefix.
    606 *
    607 * @param {Mixed} obj
    608 *        Object whose properties we want to filter.
    609 * @param {string} match
    610 *        Filter for properties that match this string.
    611 * @returns {Set} List of matched properties.
    612 */
    613 function getMatchedPropsImpl(obj, match, { chainIterator, getProperties }) {
    614  const matches = new Set();
    615  let numProps = 0;
    616 
    617  const insensitiveMatching = match && match[0].toUpperCase() !== match[0];
    618  const propertyMatches = prop => {
    619    return insensitiveMatching
    620      ? prop.toLocaleLowerCase().startsWith(match.toLocaleLowerCase())
    621      : prop.startsWith(match);
    622  };
    623 
    624  // We need to go up the prototype chain.
    625  const iter = chainIterator(obj);
    626  for (obj of iter) {
    627    const props = getProperties(obj);
    628    if (!props) {
    629      continue;
    630    }
    631    numProps += props.length;
    632 
    633    // If there are too many properties to event attempt autocompletion,
    634    // or if we have already added the max number, then stop looping
    635    // and return the partial set that has already been discovered.
    636    if (
    637      numProps >= MAX_AUTOCOMPLETE_ATTEMPTS ||
    638      matches.size >= MAX_AUTOCOMPLETIONS
    639    ) {
    640      break;
    641    }
    642 
    643    for (let i = 0; i < props.length; i++) {
    644      const prop = props[i];
    645      if (!propertyMatches(prop)) {
    646        continue;
    647      }
    648 
    649      // If it is an array index, we can't take it.
    650      // This uses a trick: converting a string to a number yields NaN if
    651      // the operation failed, and NaN is not equal to itself.
    652      // eslint-disable-next-line no-self-compare
    653      if (+prop != +prop || prop === "Infinity") {
    654        matches.add(prop);
    655      }
    656 
    657      if (matches.size >= MAX_AUTOCOMPLETIONS) {
    658        break;
    659      }
    660    }
    661  }
    662 
    663  return matches;
    664 }
    665 
    666 /**
    667 * Returns a property value based on its name from the given object, by
    668 * recursively checking the object's prototype.
    669 *
    670 * @param object obj
    671 *        An object to look the property into.
    672 * @param string name
    673 *        The property that is looked up.
    674 * @returns object|undefined
    675 *        A Debugger.Object if the property exists in the object's prototype
    676 *        chain, undefined otherwise.
    677 */
    678 function getExactMatchImpl(obj, name, { chainIterator, getProperty }) {
    679  // We need to go up the prototype chain.
    680  const iter = chainIterator(obj);
    681  for (obj of iter) {
    682    const prop = getProperty(obj, name, obj);
    683    if (prop) {
    684      return prop.value;
    685    }
    686  }
    687  return undefined;
    688 }
    689 
    690 var JSObjectSupport = {
    691  *chainIterator(obj) {
    692    while (obj) {
    693      yield obj;
    694      try {
    695        obj = Object.getPrototypeOf(obj);
    696      } catch (error) {
    697        // The above can throw e.g. for some proxy objects.
    698        return;
    699      }
    700    }
    701  },
    702 
    703  getProperties(obj) {
    704    try {
    705      return Object.getOwnPropertyNames(obj);
    706    } catch (error) {
    707      // The above can throw e.g. for some proxy objects.
    708      return null;
    709    }
    710  },
    711 
    712  getProperty() {
    713    // getProperty is unsafe with raw JS objects.
    714    throw new Error("Unimplemented!");
    715  },
    716 };
    717 
    718 var DebuggerObjectSupport = {
    719  *chainIterator(obj) {
    720    while (obj) {
    721      yield obj;
    722      try {
    723        // There could be transparent security wrappers, unwrap to check if it's a proxy.
    724        const unwrapped = DevToolsUtils.unwrap(obj);
    725        if (unwrapped === undefined) {
    726          // Objects belonging to an invisible-to-debugger compartment can't be unwrapped.
    727          return;
    728        }
    729 
    730        if (unwrapped.isProxy) {
    731          // Proxies might have a `getPrototypeOf` method, which is triggered by `obj.proto`,
    732          // but this does not impact the actual prototype chain.
    733          // In such case, we need to use the proxy target prototype.
    734          // We retrieve proxyTarget from `obj` (and not `unwrapped`) to avoid exposing
    735          // the unwrapped target.
    736          obj = unwrapped.proxyTarget;
    737        }
    738        obj = obj.proto;
    739      } catch (error) {
    740        // The above can throw e.g. for some proxy objects.
    741        return;
    742      }
    743    }
    744  },
    745 
    746  getProperties(obj) {
    747    try {
    748      return obj.getOwnPropertyNames();
    749    } catch (error) {
    750      // The above can throw e.g. for some proxy objects.
    751      return null;
    752    }
    753  },
    754 
    755  getProperty() {
    756    // This is left unimplemented in favor to DevToolsUtils.getProperty().
    757    throw new Error("Unimplemented!");
    758  },
    759 };
    760 
    761 var DebuggerEnvironmentSupport = {
    762  *chainIterator(obj) {
    763    while (obj) {
    764      yield obj;
    765      obj = obj.parent;
    766    }
    767  },
    768 
    769  getProperties(obj) {
    770    const names = obj.names();
    771 
    772    // Include 'this' in results (in sorted order)
    773    for (let i = 0; i < names.length; i++) {
    774      if (i === names.length - 1 || names[i + 1] > "this") {
    775        names.splice(i + 1, 0, "this");
    776        break;
    777      }
    778    }
    779 
    780    return names;
    781  },
    782 
    783  getProperty(obj, name) {
    784    let result;
    785    // Try/catch since name can be anything, and getVariable throws if
    786    // it's not a valid ECMAScript identifier name
    787    try {
    788      // TODO: we should use getVariableDescriptor() here - bug 725815.
    789      result = obj.getVariable(name);
    790    } catch (e) {
    791      // Ignore.
    792    }
    793 
    794    // FIXME: Need actual UI, bug 941287.
    795    if (
    796      result == null ||
    797      (typeof result == "object" &&
    798        (result.optimizedOut || result.missingArguments))
    799    ) {
    800      return null;
    801    }
    802    return { value: result };
    803  },
    804 };
    805 
    806 exports.jsPropertyProvider = DevToolsUtils.makeInfallible(jsPropertyProvider);
    807 
    808 // Export a version that will throw (for tests)
    809 exports.fallibleJsPropertyProvider = jsPropertyProvider;