tor-browser

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

autocomplete.js (12868B)


      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 {
      8  AUTOCOMPLETE_CLEAR,
      9  AUTOCOMPLETE_DATA_RECEIVE,
     10  AUTOCOMPLETE_PENDING_REQUEST,
     11  AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
     12 } = require("resource://devtools/client/webconsole/constants.js");
     13 
     14 const {
     15  analyzeInputString,
     16  shouldInputBeAutocompleted,
     17 } = require("resource://devtools/shared/webconsole/analyze-input-string.js");
     18 
     19 loader.lazyRequireGetter(
     20  this,
     21  "getSelectedTarget",
     22  "resource://devtools/shared/commands/target/selectors/targets.js",
     23  true
     24 );
     25 
     26 /**
     27 * Update the data used for the autocomplete popup in the console input (JsTerm).
     28 *
     29 * @param {boolean} force: True to force a call to the server (as opposed to retrieve
     30 *                         from the cache).
     31 * @param {Array<string>} getterPath: Array representing the getter access (i.e.
     32 *                                    `a.b.c.d.` is described as ['a', 'b', 'c', 'd'] ).
     33 * @param {Array<string>} expressionVars: Array of the variables defined in the expression.
     34 */
     35 function autocompleteUpdate(force, getterPath, expressionVars) {
     36  return async ({ dispatch, getState, webConsoleUI, hud }) => {
     37    if (hud.inputHasSelection()) {
     38      return dispatch(autocompleteClear());
     39    }
     40 
     41    const inputValue = hud.getInputValue();
     42    const mappedVars = hud.getMappedVariables() ?? {};
     43    const allVars = (expressionVars ?? []).concat(Object.keys(mappedVars));
     44    const frameActorId = await hud.getSelectedFrameActorID();
     45 
     46    const cursor = webConsoleUI.getInputCursor();
     47 
     48    const state = getState().autocomplete;
     49    const { cache } = state;
     50    if (
     51      !force &&
     52      (!inputValue || /^[a-zA-Z0-9_$]/.test(inputValue.substring(cursor)))
     53    ) {
     54      return dispatch(autocompleteClear());
     55    }
     56 
     57    const rawInput = inputValue.substring(0, cursor);
     58    const retrieveFromCache =
     59      !force &&
     60      cache &&
     61      cache.input &&
     62      rawInput.startsWith(cache.input) &&
     63      /[a-zA-Z0-9]$/.test(rawInput) &&
     64      frameActorId === cache.frameActorId;
     65 
     66    if (retrieveFromCache) {
     67      return dispatch(autoCompleteDataRetrieveFromCache(rawInput));
     68    }
     69 
     70    const authorizedEvaluations = updateAuthorizedEvaluations(
     71      state.authorizedEvaluations,
     72      getterPath,
     73      mappedVars
     74    );
     75 
     76    const { input, originalExpression } = await getMappedInput(
     77      rawInput,
     78      mappedVars,
     79      hud
     80    );
     81 
     82    return dispatch(
     83      autocompleteDataFetch({
     84        input,
     85        frameActorId,
     86        authorizedEvaluations,
     87        force,
     88        allVars,
     89        mappedVars,
     90        originalExpression,
     91      })
     92    );
     93  };
     94 }
     95 
     96 /**
     97 * Combine or replace authorizedEvaluations with the newly authorized getter path, if any.
     98 *
     99 * @param {Array<Array<string>>} authorizedEvaluations Existing authorized evaluations (may
    100 * be updated in place)
    101 * @param {Array<string>} getterPath The new getter path
    102 * @param {{[string]: string}} mappedVars Map of original to generated variable names.
    103 * @returns {Array<Array<string>>} The updated authorized evaluations (the original array,
    104 * if it was updated in place)
    105 */
    106 function updateAuthorizedEvaluations(
    107  authorizedEvaluations,
    108  getterPath,
    109  mappedVars
    110 ) {
    111  if (!Array.isArray(authorizedEvaluations) || !authorizedEvaluations.length) {
    112    authorizedEvaluations = [];
    113  }
    114 
    115  if (Array.isArray(getterPath) && getterPath.length) {
    116    // We need to check for any previous authorizations. For example, here if getterPath
    117    // is ["a", "b", "c", "d"], we want to see if there was any other path that was
    118    // authorized in a previous request. For that, we only add the previous
    119    // authorizations if the last auth is contained in getterPath. (for the example, we
    120    // would keep if it is [["a", "b"]], not if [["b"]] nor [["f", "g"]])
    121    const last = authorizedEvaluations[authorizedEvaluations.length - 1];
    122 
    123    const generatedPath = mappedVars[getterPath[0]]?.split(".");
    124    if (generatedPath) {
    125      getterPath = generatedPath.concat(getterPath.slice(1));
    126    }
    127 
    128    const isMappedVariable =
    129      generatedPath && getterPath.length === generatedPath.length;
    130    const concat = !last || last.every((x, index) => x === getterPath[index]);
    131    if (isMappedVariable) {
    132      // If the path consists only of an original variable, add all the prefixes of its
    133      // mapping. For example, for myVar => a.b.c, authorize a, a.b, and a.b.c. This
    134      // ensures we'll only show a prompt for myVar once even if a.b and a.b.c are both
    135      // unsafe getters.
    136      authorizedEvaluations = generatedPath.map((_, i) =>
    137        generatedPath.slice(0, i + 1)
    138      );
    139    } else if (concat) {
    140      authorizedEvaluations.push(getterPath);
    141    } else {
    142      authorizedEvaluations = [getterPath];
    143    }
    144  }
    145  return authorizedEvaluations;
    146 }
    147 
    148 /**
    149 * Apply source mapping to the autocomplete input.
    150 *
    151 * @param {string} rawInput The input to map.
    152 * @param {{[string]: string}} mappedVars Map of original to generated variable names.
    153 * @param {WebConsole} hud A reference to the webconsole hud.
    154 * @returns {string} The source-mapped expression to autocomplete.
    155 */
    156 async function getMappedInput(rawInput, mappedVars, hud) {
    157  if (!mappedVars || !Object.keys(mappedVars).length) {
    158    return { input: rawInput, originalExpression: undefined };
    159  }
    160 
    161  const inputAnalysis = analyzeInputString(rawInput, 500);
    162  if (!shouldInputBeAutocompleted(inputAnalysis)) {
    163    return { input: rawInput, originalExpression: undefined };
    164  }
    165 
    166  const {
    167    mainExpression: originalExpression,
    168    isPropertyAccess,
    169    isElementAccess,
    170    lastStatement,
    171  } = inputAnalysis;
    172 
    173  // If we're autocompleting a variable name, pass it through unchanged so that we
    174  // show original variable names rather than generated ones.
    175  // For example, if we have the mapping `myVariable` => `x`, show variables starting
    176  // with myVariable rather than x.
    177  if (!isPropertyAccess && !isElementAccess) {
    178    return { input: lastStatement, originalExpression };
    179  }
    180 
    181  let generated =
    182    (await hud.getMappedExpression(originalExpression))?.expression ??
    183    originalExpression;
    184  // Strip off the semicolon if the expression was converted to a statement
    185  const trailingSemicolon = /;\s*$/;
    186  if (
    187    trailingSemicolon.test(generated) &&
    188    !trailingSemicolon.test(originalExpression)
    189  ) {
    190    generated = generated.slice(0, generated.lastIndexOf(";"));
    191  }
    192 
    193  const suffix = lastStatement.slice(originalExpression.length);
    194  return { input: generated + suffix, originalExpression };
    195 }
    196 
    197 /**
    198 * Called when the autocompletion data should be cleared.
    199 */
    200 function autocompleteClear() {
    201  return {
    202    type: AUTOCOMPLETE_CLEAR,
    203  };
    204 }
    205 
    206 /**
    207 * Called when the autocompletion data should be retrieved from the cache (i.e.
    208 * client-side).
    209 *
    210 * @param {string} input: The input used to filter the cached data.
    211 */
    212 function autoCompleteDataRetrieveFromCache(input) {
    213  return {
    214    type: AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
    215    input,
    216  };
    217 }
    218 
    219 let currentRequestId = 0;
    220 function generateRequestId() {
    221  return currentRequestId++;
    222 }
    223 
    224 /**
    225 * Action that fetch autocompletion data from the server.
    226 *
    227 * @param {object} Object of the following shape:
    228 *        - {String} input: the expression that we want to complete.
    229 *        - {String} frameActorId: The id of the frame we want to autocomplete in.
    230 *        - {Boolean} force: true if the user forced an autocompletion (with Ctrl+Space).
    231 *        - {Array} authorizedEvaluations: Array of the properties access which can be
    232 *                  executed by the engine.
    233 *                   Example: [["x", "myGetter"], ["x", "myGetter", "y", "glitter"]]
    234 *                  to retrieve properties of `x.myGetter.` and `x.myGetter.y.glitter`.
    235 */
    236 function autocompleteDataFetch({
    237  input,
    238  frameActorId,
    239  force,
    240  authorizedEvaluations,
    241  allVars,
    242  mappedVars,
    243  originalExpression,
    244 }) {
    245  return async ({ dispatch, commands, webConsoleUI, hud }) => {
    246    // Retrieve the right WebConsole front that relates either to (by order of priority):
    247    // - the currently selected target in the context selector
    248    //   (contextSelectedTargetFront),
    249    // - the currently selected Node in the inspector (selectedNodeActor),
    250    // - the currently selected frame in the debugger (when paused) (frameActor),
    251    // - the currently selected target in the iframe dropdown
    252    //   (selectedTargetFront from the TargetCommand)
    253    const selectedNodeActorId = webConsoleUI.getSelectedNodeActorID();
    254 
    255    let targetFront = commands.targetCommand.selectedTargetFront;
    256    // Note that getSelectedTargetFront will return null if we default to the top level target.
    257    const contextSelectorTargetFront = getSelectedTarget(
    258      hud.commands.targetCommand.store.getState()
    259    );
    260    const selectedActorId = selectedNodeActorId || frameActorId;
    261    if (contextSelectorTargetFront) {
    262      targetFront = contextSelectorTargetFront;
    263    } else if (selectedActorId) {
    264      const selectedFront = commands.client.getFrontByID(selectedActorId);
    265      if (selectedFront) {
    266        targetFront = selectedFront.targetFront;
    267      }
    268    }
    269 
    270    const webconsoleFront = await targetFront.getFront("console");
    271 
    272    const id = generateRequestId();
    273    dispatch({ type: AUTOCOMPLETE_PENDING_REQUEST, id });
    274 
    275    webconsoleFront
    276      .autocomplete(
    277        input,
    278        undefined,
    279        frameActorId,
    280        selectedNodeActorId,
    281        authorizedEvaluations,
    282        allVars
    283      )
    284      .then(data => {
    285        if (data.isUnsafeGetter && originalExpression !== undefined) {
    286          data.getterPath = unmapGetterPath(
    287            data.getterPath,
    288            originalExpression,
    289            mappedVars
    290          );
    291        }
    292        return dispatch(
    293          autocompleteDataReceive({
    294            id,
    295            input,
    296            force,
    297            frameActorId,
    298            data,
    299            authorizedEvaluations,
    300          })
    301        );
    302      })
    303      .catch(e => {
    304        console.error("failed autocomplete", e);
    305        dispatch(autocompleteClear());
    306      });
    307  };
    308 }
    309 
    310 /**
    311 * Replace generated variable names in an unsafe getter path with their original
    312 * counterparts.
    313 *
    314 * @param {Array<string>} getterPath Array of properties leading up to and including the
    315 * unsafe getter.
    316 * @param {string} originalExpression The expression that was evaluated, before mapping.
    317 * @param {{[string]: string}} mappedVars Map of original to generated variable names.
    318 * @returns {Array<string>} An updated getter path containing original variables.
    319 */
    320 function unmapGetterPath(getterPath, originalExpression, mappedVars) {
    321  // We know that the original expression is a sequence of property accesses, that only
    322  // the first part can be a mapped variable, and that the getter path must start with
    323  // its generated path or be a prefix of it.
    324 
    325  // Suppose we have the expression `foo.bar`, which maps to `a.b.c.bar`.
    326  // Get the first part of the expression ("foo")
    327  const originalVariable = /^[^.[?]*/s.exec(originalExpression)[0].trim();
    328  const generatedVariable = mappedVars[originalVariable];
    329  if (generatedVariable) {
    330    // Get number of properties in "a.b.c"
    331    const generatedVariableParts = generatedVariable.split(".");
    332    // Replace ["a", "b", "c"] with "foo" in the getter path.
    333    // Note that this will also work if the getter path ends inside of the mapped
    334    // variable, like ["a", "b"].
    335    return [
    336      originalVariable,
    337      ...getterPath.slice(generatedVariableParts.length),
    338    ];
    339  }
    340  return getterPath;
    341 }
    342 
    343 /**
    344 * Called when we receive the autocompletion data from the server.
    345 *
    346 * @param {object} Object of the following shape:
    347 *        - {Integer} id: The autocompletion request id. This will be used in the reducer
    348 *                        to check that we update the state with the last request results.
    349 *        - {String} input: the expression that we want to complete.
    350 *        - {String} frameActorId: The id of the frame we want to autocomplete in.
    351 *        - {Boolean} force: true if the user forced an autocompletion (with Ctrl+Space).
    352 *        - {Object} data: The actual data returned from the server.
    353 *        - {Array} authorizedEvaluations: Array of the properties access which can be
    354 *                  executed by the engine.
    355 *                   Example: [["x", "myGetter"], ["x", "myGetter", "y", "glitter"]]
    356 *                  to retrieve properties of `x.myGetter.` and `x.myGetter.y.glitter`.
    357 */
    358 function autocompleteDataReceive({
    359  id,
    360  input,
    361  frameActorId,
    362  force,
    363  data,
    364  authorizedEvaluations,
    365 }) {
    366  return {
    367    type: AUTOCOMPLETE_DATA_RECEIVE,
    368    id,
    369    input,
    370    force,
    371    frameActorId,
    372    data,
    373    authorizedEvaluations,
    374  };
    375 }
    376 
    377 module.exports = {
    378  autocompleteClear,
    379  autocompleteUpdate,
    380 };