tor-browser

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

context-menu.js (11892B)


      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 Menu = require("resource://devtools/client/framework/menu.js");
      8 const MenuItem = require("resource://devtools/client/framework/menu-item.js");
      9 
     10 const {
     11  MESSAGE_SOURCE,
     12 } = require("resource://devtools/client/webconsole/constants.js");
     13 
     14 const clipboardHelper = require("resource://devtools/shared/platform/clipboard.js");
     15 const {
     16  l10n,
     17 } = require("resource://devtools/client/webconsole/utils/messages.js");
     18 const actions = require("resource://devtools/client/webconsole/actions/index.js");
     19 
     20 loader.lazyRequireGetter(
     21  this,
     22  "saveAs",
     23  "resource://devtools/shared/DevToolsUtils.js",
     24  true
     25 );
     26 loader.lazyRequireGetter(
     27  this,
     28  "openContentLink",
     29  "resource://devtools/client/shared/link.js",
     30  true
     31 );
     32 loader.lazyRequireGetter(
     33  this,
     34  "getElementText",
     35  "resource://devtools/client/webconsole/utils/clipboard.js",
     36  true
     37 );
     38 
     39 /**
     40 * Create a Menu instance for the webconsole.
     41 *
     42 * @param {Event} context menu event
     43 *        {object} message (optional) message object containing metadata such as:
     44 *        - {String} source
     45 *        - {String} request
     46 * @param {object} options
     47 *        - {Actions} bound actions
     48 *        - {WebConsoleWrapper} wrapper instance used for accessing properties like the store
     49 *          and window.
     50 */
     51 function createContextMenu(event, message, webConsoleWrapper) {
     52  const { target } = event;
     53  const { parentNode, toolbox, hud } = webConsoleWrapper;
     54  const store = webConsoleWrapper.getStore();
     55  const { dispatch } = store;
     56 
     57  const messageEl = target.closest(".message");
     58  const clipboardText = getElementText(messageEl);
     59 
     60  const linkEl = target.closest("a[href]");
     61  const url = linkEl && linkEl.href;
     62 
     63  const messageVariable = target.closest(".objectBox");
     64  // Ensure that console.group and console.groupCollapsed commands are not captured
     65  const variableText =
     66    messageVariable &&
     67    !messageEl.classList.contains("startGroup") &&
     68    !messageEl.classList.contains("startGroupCollapsed")
     69      ? messageVariable.textContent
     70      : null;
     71 
     72  // Retrieve closes actor id from the DOM.
     73  const actorEl =
     74    target.closest("[data-link-actor-id]") ||
     75    target.querySelector("[data-link-actor-id]");
     76  const actor = actorEl ? actorEl.dataset.linkActorId : null;
     77 
     78  const rootObjectInspector = target.closest(".object-inspector");
     79  const rootActor = rootObjectInspector
     80    ? rootObjectInspector.querySelector("[data-link-actor-id]")
     81    : null;
     82  // We can have object which are not displayed inside an ObjectInspector (e.g. Errors),
     83  // so let's default to `actor`.
     84  const rootActorId = rootActor ? rootActor.dataset.linkActorId : actor;
     85 
     86  const elementNode =
     87    target.closest(".objectBox-node") || target.closest(".objectBox-textNode");
     88  const isConnectedElement =
     89    elementNode && elementNode.querySelector(".open-inspector") !== null;
     90 
     91  const win = parentNode.ownerDocument.defaultView;
     92  const selection = win.getSelection();
     93 
     94  const { source, request, messageId } = message || {};
     95 
     96  const menu = new Menu({ id: "webconsole-menu" });
     97 
     98  // Copy URL for a network request.
     99  menu.append(
    100    new MenuItem({
    101      id: "console-menu-copy-url",
    102      label: l10n.getStr("webconsole.menu.copyURL.label"),
    103      accesskey: l10n.getStr("webconsole.menu.copyURL.accesskey"),
    104      visible: source === MESSAGE_SOURCE.NETWORK,
    105      click: () => {
    106        if (!request) {
    107          return;
    108        }
    109        clipboardHelper.copyString(request.url);
    110      },
    111    })
    112  );
    113 
    114  if (toolbox && request) {
    115    // Open Network message in the Network panel.
    116    menu.append(
    117      new MenuItem({
    118        id: "console-menu-open-in-network-panel",
    119        label: l10n.getStr("webconsole.menu.openInNetworkPanel.label"),
    120        accesskey: l10n.getStr("webconsole.menu.openInNetworkPanel.accesskey"),
    121        visible: source === MESSAGE_SOURCE.NETWORK,
    122        click: () => dispatch(actions.openNetworkPanel(message.messageId)),
    123      })
    124    );
    125    // Resend Network message.
    126    menu.append(
    127      new MenuItem({
    128        id: "console-menu-resend-network-request",
    129        label: l10n.getStr("webconsole.menu.resendNetworkRequest.label"),
    130        accesskey: l10n.getStr(
    131          "webconsole.menu.resendNetworkRequest.accesskey"
    132        ),
    133        visible: source === MESSAGE_SOURCE.NETWORK,
    134        click: () => dispatch(actions.resendNetworkRequest(messageId)),
    135      })
    136    );
    137  }
    138 
    139  // Open URL in a new tab for a network request.
    140  menu.append(
    141    new MenuItem({
    142      id: "console-menu-open-url",
    143      label: l10n.getStr("webconsole.menu.openURL.label"),
    144      accesskey: l10n.getStr("webconsole.menu.openURL.accesskey"),
    145      visible: source === MESSAGE_SOURCE.NETWORK,
    146      click: () => {
    147        if (!request) {
    148          return;
    149        }
    150        openContentLink(request.url);
    151      },
    152    })
    153  );
    154 
    155  // Open DOM node in the Inspector panel.
    156  const contentDomReferenceEl = target.closest(
    157    "[data-link-content-dom-reference]"
    158  );
    159  if (isConnectedElement && contentDomReferenceEl) {
    160    const contentDomReference = contentDomReferenceEl.getAttribute(
    161      "data-link-content-dom-reference"
    162    );
    163 
    164    menu.append(
    165      new MenuItem({
    166        id: "console-menu-open-node",
    167        label: l10n.getStr("webconsole.menu.openNodeInInspector.label"),
    168        accesskey: l10n.getStr("webconsole.menu.openNodeInInspector.accesskey"),
    169        disabled: false,
    170        click: () =>
    171          dispatch(
    172            actions.openNodeInInspector(JSON.parse(contentDomReference))
    173          ),
    174      })
    175    );
    176  }
    177 
    178  // Store as global variable.
    179  menu.append(
    180    new MenuItem({
    181      id: "console-menu-store",
    182      label: l10n.getStr("webconsole.menu.storeAsGlobalVar.label"),
    183      accesskey: l10n.getStr("webconsole.menu.storeAsGlobalVar.accesskey"),
    184      disabled: !actor,
    185      click: () => dispatch(actions.storeAsGlobal(actor)),
    186    })
    187  );
    188 
    189  // Copy message or grip.
    190  menu.append(
    191    new MenuItem({
    192      id: "console-menu-copy",
    193      label: l10n.getStr("webconsole.menu.copyMessage.label"),
    194      accesskey: l10n.getStr("webconsole.menu.copyMessage.accesskey"),
    195      // Disabled if there is no selection and no message element available to copy.
    196      disabled: selection.isCollapsed && !clipboardText,
    197      click: () => {
    198        if (selection.isCollapsed) {
    199          // If the selection is empty/collapsed, copy the text content of the
    200          // message for which the context menu was opened.
    201          clipboardHelper.copyString(clipboardText);
    202        } else {
    203          clipboardHelper.copyString(selection.toString());
    204        }
    205      },
    206    })
    207  );
    208 
    209  // Copy message object.
    210  menu.append(
    211    new MenuItem({
    212      id: "console-menu-copy-object",
    213      label: l10n.getStr("webconsole.menu.copyObject.label"),
    214      accesskey: l10n.getStr("webconsole.menu.copyObject.accesskey"),
    215      // Disabled if there is no actor and no variable text associated.
    216      disabled: !actor && !variableText,
    217      click: () => dispatch(actions.copyMessageObject(actor, variableText)),
    218    })
    219  );
    220 
    221  // Export to clipboard
    222  menu.append(
    223    new MenuItem({
    224      id: "console-menu-export-clipboard",
    225      label: l10n.getStr("webconsole.menu.copyAllMessages.label"),
    226      accesskey: l10n.getStr("webconsole.menu.copyAllMessages.accesskey"),
    227      disabled: false,
    228      async click() {
    229        const outputText =
    230          await getUnvirtualizedConsoleOutputText(webConsoleWrapper);
    231        clipboardHelper.copyString(outputText);
    232      },
    233    })
    234  );
    235 
    236  // Export to file
    237  menu.append(
    238    new MenuItem({
    239      id: "console-menu-export-file",
    240      label: l10n.getStr("webconsole.menu.saveAllMessagesFile.label"),
    241      accesskey: l10n.getStr("webconsole.menu.saveAllMessagesFile.accesskey"),
    242      disabled: false,
    243      // Note: not async, but returns a promise for the actual save.
    244      click: async () => {
    245        const date = new Date();
    246        const suggestedName =
    247          `console-export-${date.getFullYear()}-` +
    248          `${date.getMonth() + 1}-${date.getDate()}_${date.getHours()}-` +
    249          `${date.getMinutes()}-${date.getSeconds()}.log`;
    250        const outputText =
    251          await getUnvirtualizedConsoleOutputText(webConsoleWrapper);
    252        const data = new TextEncoder().encode(outputText);
    253        saveAs(window, data, suggestedName);
    254      },
    255    })
    256  );
    257 
    258  // Open object in sidebar.
    259  const shouldOpenSidebar = store.getState().prefs.sidebarToggle;
    260  if (shouldOpenSidebar) {
    261    menu.append(
    262      new MenuItem({
    263        id: "console-menu-open-sidebar",
    264        label: l10n.getStr("webconsole.menu.openInSidebar.label1"),
    265        accesskey: l10n.getStr("webconsole.menu.openInSidebar.accesskey"),
    266        disabled: !rootActorId,
    267        click: () => dispatch(actions.openSidebar(messageId, rootActorId)),
    268      })
    269    );
    270  }
    271 
    272  if (url) {
    273    menu.append(
    274      new MenuItem({
    275        id: "console-menu-open-url",
    276        label: l10n.getStr("webconsole.menu.openURL.label"),
    277        accesskey: l10n.getStr("webconsole.menu.openURL.accesskey"),
    278        click: () =>
    279          openContentLink(url, {
    280            inBackground: true,
    281            relatedToCurrent: true,
    282          }),
    283      })
    284    );
    285    menu.append(
    286      new MenuItem({
    287        id: "console-menu-copy-url",
    288        label: l10n.getStr("webconsole.menu.copyURL.label"),
    289        accesskey: l10n.getStr("webconsole.menu.copyURL.accesskey"),
    290        click: () => clipboardHelper.copyString(url),
    291      })
    292    );
    293  }
    294 
    295  // Emit the "menu-open" event for testing.
    296  const { screenX, screenY } = event;
    297  menu.once("open", () => webConsoleWrapper.emitForTests("menu-open"));
    298  menu.popup(screenX, screenY, hud.chromeWindow.document);
    299 
    300  return menu;
    301 }
    302 
    303 exports.createContextMenu = createContextMenu;
    304 
    305 /**
    306 * Returns the whole text content of the console output.
    307 * We're creating a new ConsoleOutput using the current store, turning off virtualization
    308 * so we can have access to all the messages.
    309 *
    310 * @param {WebConsoleWrapper} webConsoleWrapper
    311 * @returns Promise<String>
    312 */
    313 async function getUnvirtualizedConsoleOutputText(webConsoleWrapper) {
    314  return new Promise(resolve => {
    315    const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.mjs");
    316    const {
    317      createElement,
    318      createFactory,
    319    } = require("resource://devtools/client/shared/vendor/react.mjs");
    320    const ConsoleOutput = createFactory(
    321      require("resource://devtools/client/webconsole/components/Output/ConsoleOutput.js")
    322    );
    323    const {
    324      Provider,
    325      createProvider,
    326    } = require("resource://devtools/client/shared/vendor/react-redux.js");
    327 
    328    const { parentNode, toolbox } = webConsoleWrapper;
    329    const doc = parentNode.ownerDocument;
    330 
    331    // Create an element that won't impact the layout of the console
    332    const singleUseElement = doc.createElement("section");
    333    singleUseElement.classList.add("clipboard-only");
    334    doc.body.append(singleUseElement);
    335 
    336    const consoleOutput = ConsoleOutput({
    337      serviceContainer: {
    338        ...webConsoleWrapper.getServiceContainer(),
    339        preventStacktraceInitialRenderDelay: true,
    340      },
    341      disableVirtualization: true,
    342    });
    343 
    344    ReactDOM.render(
    345      createElement(
    346        Provider,
    347        {
    348          store: webConsoleWrapper.getStore(),
    349        },
    350        toolbox
    351          ? createElement(
    352              createProvider(toolbox.commands.targetCommand.storeId),
    353              { store: toolbox.commands.targetCommand.store },
    354              consoleOutput
    355            )
    356          : consoleOutput
    357      ),
    358      singleUseElement,
    359      () => {
    360        resolve(getElementText(singleUseElement));
    361        singleUseElement.remove();
    362        ReactDOM.unmountComponentAtNode(singleUseElement);
    363      }
    364    );
    365  });
    366 }