tor-browser

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

browser-menus.js (9326B)


      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 /**
      8 * This module inject dynamically menu items into browser UI.
      9 *
     10 * Menu definitions are fetched from:
     11 * - devtools/client/menus for top level entires
     12 * - devtools/client/definitions for tool-specifics entries
     13 */
     14 
     15 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     16 const MENUS_L10N = new LocalizationHelper(
     17  "devtools/client/locales/menus.properties"
     18 );
     19 
     20 loader.lazyRequireGetter(
     21  this,
     22  "gDevTools",
     23  "resource://devtools/client/framework/devtools.js",
     24  true
     25 );
     26 loader.lazyRequireGetter(
     27  this,
     28  "gDevToolsBrowser",
     29  "resource://devtools/client/framework/devtools-browser.js",
     30  true
     31 );
     32 loader.lazyRequireGetter(
     33  this,
     34  "Telemetry",
     35  "resource://devtools/client/shared/telemetry.js"
     36 );
     37 
     38 let telemetry = null;
     39 
     40 // Keep list of inserted DOM Elements in order to remove them on unload
     41 // Maps browser xul document => list of DOM Elements
     42 const FragmentsCache = new Map();
     43 
     44 function l10n(key) {
     45  return MENUS_L10N.getStr(key);
     46 }
     47 
     48 /**
     49 * Create a xul:menuitem element
     50 *
     51 * @param {HTMLDocument} doc
     52 *        The document to which menus are to be added.
     53 * @param {string} id
     54 *        Element id.
     55 * @param {string} label
     56 *        Menu label.
     57 * @param {string} accesskey (optional)
     58 *        Access key of the menuitem, used as shortcut while opening the menu.
     59 * @param {boolean} isCheckbox (optional)
     60 *        If true, the menuitem will act as a checkbox and have an optional
     61 *        tick on its left.
     62 * @param {string} appMenuL10nId (optional)
     63 *        A Fluent key to set the appmenu-data-l10n-id attribute of the menuitem
     64 *        to. This can then be used to show a different string when cloning the
     65 *        menuitem to show in the AppMenu or panel contexts.
     66 *
     67 * @return XULMenuItemElement
     68 */
     69 function createMenuItem({
     70  doc,
     71  id,
     72  label,
     73  accesskey,
     74  isCheckbox,
     75  appMenuL10nId,
     76 }) {
     77  const menuitem = doc.createXULElement("menuitem");
     78  menuitem.id = id;
     79  menuitem.setAttribute("label", label);
     80  if (accesskey) {
     81    menuitem.setAttribute("accesskey", accesskey);
     82  }
     83  if (isCheckbox) {
     84    menuitem.setAttribute("type", "checkbox");
     85    menuitem.setAttribute("autocheck", "false");
     86  }
     87  if (appMenuL10nId) {
     88    menuitem.setAttribute("appmenu-data-l10n-id", appMenuL10nId);
     89  }
     90  return menuitem;
     91 }
     92 
     93 /**
     94 * Add a menu entry for a tool definition
     95 *
     96 * @param {object} toolDefinition
     97 *        Tool definition of the tool to add a menu entry.
     98 * @param {HTMLDocument} doc
     99 *        The document to which the tool menu item is to be added.
    100 */
    101 function createToolMenuElements(toolDefinition, doc) {
    102  const id = toolDefinition.id;
    103  const menuId = "menuitem_" + id;
    104 
    105  // Prevent multiple entries for the same tool.
    106  if (doc.getElementById(menuId)) {
    107    return null;
    108  }
    109 
    110  const oncommand = async function (id, event) {
    111    try {
    112      const window = event.target.ownerDocument.defaultView;
    113      await gDevToolsBrowser.selectToolCommand(window, id, ChromeUtils.now());
    114      sendEntryPointTelemetry(window);
    115    } catch (e) {
    116      console.error(`Exception while opening ${id}: ${e}\n${e.stack}`);
    117    }
    118  }.bind(null, id);
    119 
    120  const menuitem = createMenuItem({
    121    doc,
    122    id: "menuitem_" + id,
    123    label: toolDefinition.menuLabel || toolDefinition.label,
    124    accesskey: toolDefinition.accesskey,
    125    appMenuL10nId: toolDefinition.appMenuL10nId,
    126  });
    127  // Refer to the key in order to display the key shortcut at menu ends
    128  // This <key> element is being created by devtools/client/devtools-startup.js
    129  menuitem.setAttribute("key", "key_" + id);
    130  menuitem.addEventListener("command", oncommand);
    131 
    132  return menuitem;
    133 }
    134 
    135 /**
    136 * Send entry point telemetry explaining how the devtools were launched when
    137 * launched from the System Menu.. This functionality also lives inside
    138 * `devtools/startup/devtools-startup.js` but that codepath is only used the
    139 * first time a toolbox is opened for a tab.
    140 */
    141 function sendEntryPointTelemetry(window) {
    142  if (!telemetry) {
    143    telemetry = new Telemetry();
    144  }
    145 
    146  telemetry.addEventProperty(window, "open", "tools", null, "shortcut", "");
    147 
    148  telemetry.addEventProperty(
    149    window,
    150    "open",
    151    "tools",
    152    null,
    153    "entrypoint",
    154    "SystemMenu"
    155  );
    156 }
    157 
    158 /**
    159 * Create xul menuitem, key elements for a given tool.
    160 * And then insert them into browser DOM.
    161 *
    162 * @param {HTMLDocument} doc
    163 *        The document to which the tool is to be registered.
    164 * @param {object} toolDefinition
    165 *        Tool definition of the tool to register.
    166 * @param {object} prevDef
    167 *        The tool definition after which the tool menu item is to be added.
    168 */
    169 function insertToolMenuElements(doc, toolDefinition, prevDef) {
    170  const menuitem = createToolMenuElements(toolDefinition, doc);
    171  if (!menuitem) {
    172    return;
    173  }
    174 
    175  let ref;
    176  if (prevDef) {
    177    const menuitem = doc.getElementById("menuitem_" + prevDef.id);
    178    ref = menuitem?.nextSibling ? menuitem.nextSibling : null;
    179  } else {
    180    ref = doc.getElementById("menu_devtools_remotedebugging");
    181  }
    182 
    183  if (ref) {
    184    ref.parentNode.insertBefore(menuitem, ref);
    185  }
    186 }
    187 exports.insertToolMenuElements = insertToolMenuElements;
    188 
    189 /**
    190 * Remove a tool's menuitem from a window
    191 *
    192 * @param {string} toolId
    193 *        Id of the tool to add a menu entry for
    194 * @param {HTMLDocument} doc
    195 *        The document to which the tool menu item is to be removed from
    196 */
    197 function removeToolFromMenu(toolId, doc) {
    198  const key = doc.getElementById("key_" + toolId);
    199  if (key) {
    200    key.remove();
    201  }
    202 
    203  const menuitem = doc.getElementById("menuitem_" + toolId);
    204  if (menuitem) {
    205    menuitem.remove();
    206  }
    207 }
    208 exports.removeToolFromMenu = removeToolFromMenu;
    209 
    210 /**
    211 * Add all tools to the developer tools menu of a window.
    212 *
    213 * @param {HTMLDocument} doc
    214 *        The document to which the tool items are to be added.
    215 */
    216 function addAllToolsToMenu(doc) {
    217  const fragMenuItems = doc.createDocumentFragment();
    218 
    219  for (const toolDefinition of gDevTools.getToolDefinitionArray()) {
    220    if (!toolDefinition.inMenu) {
    221      continue;
    222    }
    223 
    224    const menuItem = createToolMenuElements(toolDefinition, doc);
    225 
    226    if (!menuItem) {
    227      continue;
    228    }
    229 
    230    fragMenuItems.appendChild(menuItem);
    231  }
    232 
    233  const mps = doc.getElementById("menu_devtools_remotedebugging");
    234  if (mps) {
    235    mps.parentNode.insertBefore(fragMenuItems, mps);
    236  }
    237 }
    238 
    239 /**
    240 * Add global menus that are not panel specific.
    241 *
    242 * @param {HTMLDocument} doc
    243 *        The document to which menus are to be added.
    244 */
    245 function addTopLevelItems(doc) {
    246  const menuItems = doc.createDocumentFragment();
    247 
    248  const { menuitems } = require("resource://devtools/client/menus.js");
    249  for (const item of menuitems) {
    250    if (item.separator) {
    251      const separator = doc.createXULElement("menuseparator");
    252      separator.id = item.id;
    253      menuItems.appendChild(separator);
    254    } else {
    255      const { id, l10nKey } = item;
    256 
    257      // Create a <menuitem>
    258      const menuitem = createMenuItem({
    259        doc,
    260        id,
    261        label: l10n(l10nKey + ".label"),
    262        accesskey: l10n(l10nKey + ".accesskey"),
    263        isCheckbox: item.checkbox,
    264        appMenuL10nId: item.appMenuL10nId,
    265      });
    266      menuitem.addEventListener("command", item.oncommand);
    267      menuItems.appendChild(menuitem);
    268 
    269      if (item.keyId) {
    270        menuitem.setAttribute("key", "key_" + item.keyId);
    271      }
    272    }
    273  }
    274 
    275  // Cache all nodes before insertion to be able to remove them on unload
    276  const nodes = [];
    277  for (const node of menuItems.children) {
    278    nodes.push(node);
    279  }
    280  FragmentsCache.set(doc, nodes);
    281 
    282  const menu = doc.getElementById("menuWebDeveloperPopup");
    283  menu.appendChild(menuItems);
    284 
    285  // There is still "Page Source" and "Task Manager" menuitems hardcoded
    286  // into browser.xhtml. Instead of manually inserting everything around it,
    287  // move them to the expected position.
    288  const pageSourceMenu = doc.getElementById("menu_pageSource");
    289  const extensionsForDevelopersMenu = doc.getElementById(
    290    "extensionsForDevelopers"
    291  );
    292  menu.insertBefore(pageSourceMenu, extensionsForDevelopersMenu);
    293 
    294  const taskManagerMenu = doc.getElementById("menu_taskManager");
    295  const remoteDebuggingMenu = doc.getElementById(
    296    "menu_devtools_remotedebugging"
    297  );
    298  menu.insertBefore(taskManagerMenu, remoteDebuggingMenu);
    299 }
    300 
    301 /**
    302 * Remove global menus that are not panel specific.
    303 *
    304 * @param {HTMLDocument} doc
    305 *        The document to which menus are to be added.
    306 */
    307 function removeTopLevelItems(doc) {
    308  const nodes = FragmentsCache.get(doc);
    309  if (!nodes) {
    310    return;
    311  }
    312  FragmentsCache.delete(doc);
    313  for (const node of nodes) {
    314    node.remove();
    315  }
    316 }
    317 
    318 /**
    319 * Add menus to a browser document
    320 *
    321 * @param {HTMLDocument} doc
    322 *        The document to which menus are to be added.
    323 */
    324 exports.addMenus = function (doc) {
    325  addTopLevelItems(doc);
    326 
    327  addAllToolsToMenu(doc);
    328 };
    329 
    330 /**
    331 * Remove menus from a browser document
    332 *
    333 * @param {HTMLDocument} doc
    334 *        The document to which menus are to be removed.
    335 */
    336 exports.removeMenus = function (doc) {
    337  // We only remove top level entries. Per-tool entries are removed while
    338  // unregistering each tool.
    339  removeTopLevelItems(doc);
    340 };