tor-browser

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

toolbox-options.js (20529B)


      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 EventEmitter = require("resource://devtools/shared/event-emitter.js");
      8 const {
      9  gDevTools,
     10 } = require("resource://devtools/client/framework/devtools.js");
     11 
     12 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     13 const L10N = new LocalizationHelper(
     14  "devtools/client/locales/toolbox.properties"
     15 );
     16 
     17 loader.lazyRequireGetter(
     18  this,
     19  "openDocLink",
     20  "resource://devtools/client/shared/link.js",
     21  true
     22 );
     23 
     24 function GetPref(name) {
     25  const type = Services.prefs.getPrefType(name);
     26  switch (type) {
     27    case Services.prefs.PREF_STRING:
     28      return Services.prefs.getCharPref(name);
     29    case Services.prefs.PREF_INT:
     30      return Services.prefs.getIntPref(name);
     31    case Services.prefs.PREF_BOOL:
     32      return Services.prefs.getBoolPref(name);
     33    default:
     34      throw new Error("Unknown type");
     35  }
     36 }
     37 
     38 function SetPref(name, value) {
     39  const type = Services.prefs.getPrefType(name);
     40  switch (type) {
     41    case Services.prefs.PREF_STRING:
     42      return Services.prefs.setCharPref(name, value);
     43    case Services.prefs.PREF_INT:
     44      return Services.prefs.setIntPref(name, value);
     45    case Services.prefs.PREF_BOOL:
     46      return Services.prefs.setBoolPref(name, value);
     47    default:
     48      throw new Error("Unknown type");
     49  }
     50 }
     51 
     52 function InfallibleGetBoolPref(key) {
     53  try {
     54    return Services.prefs.getBoolPref(key);
     55  } catch (ex) {
     56    return true;
     57  }
     58 }
     59 
     60 /**
     61 * Represents the Options Panel in the Toolbox.
     62 */
     63 class OptionsPanel extends EventEmitter {
     64  constructor(iframeWindow, toolbox, commands) {
     65    super();
     66 
     67    this.panelDoc = iframeWindow.document;
     68    this.panelWin = iframeWindow;
     69 
     70    this.toolbox = toolbox;
     71    this.commands = commands;
     72    this.telemetry = toolbox.telemetry;
     73 
     74    this.setupToolsList = this.setupToolsList.bind(this);
     75 
     76    this.disableJSNode = this.panelDoc.getElementById(
     77      "devtools-disable-javascript"
     78    );
     79 
     80    this.#addListeners();
     81  }
     82 
     83  get target() {
     84    return this.toolbox.target;
     85  }
     86 
     87  async open() {
     88    this.setupToolsList();
     89    this.setupToolbarButtonsList();
     90    this.setupThemeList();
     91    this.setupAdditionalOptions();
     92    await this.populatePreferences();
     93    return this;
     94  }
     95 
     96  #addListeners() {
     97    Services.prefs.addObserver("devtools.cache.disabled", this.#prefChanged);
     98    Services.prefs.addObserver("devtools.theme", this.#prefChanged);
     99    Services.prefs.addObserver(
    100      "devtools.source-map.client-service.enabled",
    101      this.#prefChanged
    102    );
    103    Services.prefs.addObserver(
    104      "devtools.toolbox.splitconsole.enabled",
    105      this.#prefChanged
    106    );
    107    gDevTools.on("theme-registered", this.#themeRegistered);
    108    gDevTools.on("theme-unregistered", this.#themeUnregistered);
    109 
    110    // Refresh the tools list when a new tool or webextension has been
    111    // registered to the toolbox.
    112    this.toolbox.on("tool-registered", this.setupToolsList);
    113    this.toolbox.on("webextension-registered", this.setupToolsList);
    114    // Refresh the tools list when a new tool or webextension has been
    115    // unregistered from the toolbox.
    116    this.toolbox.on("tool-unregistered", this.setupToolsList);
    117    this.toolbox.on("webextension-unregistered", this.setupToolsList);
    118  }
    119 
    120  #removeListeners() {
    121    Services.prefs.removeObserver("devtools.cache.disabled", this.#prefChanged);
    122    Services.prefs.removeObserver("devtools.theme", this.#prefChanged);
    123    Services.prefs.removeObserver(
    124      "devtools.source-map.client-service.enabled",
    125      this.#prefChanged
    126    );
    127    Services.prefs.removeObserver(
    128      "devtools.toolbox.splitconsole.enabled",
    129      this.#prefChanged
    130    );
    131 
    132    this.toolbox.off("tool-registered", this.setupToolsList);
    133    this.toolbox.off("tool-unregistered", this.setupToolsList);
    134    this.toolbox.off("webextension-registered", this.setupToolsList);
    135    this.toolbox.off("webextension-unregistered", this.setupToolsList);
    136 
    137    gDevTools.off("theme-registered", this.#themeRegistered);
    138    gDevTools.off("theme-unregistered", this.#themeUnregistered);
    139  }
    140 
    141  #prefChanged = (subject, topic, prefName) => {
    142    if (prefName === "devtools.cache.disabled") {
    143      const cacheDisabled = GetPref(prefName);
    144      const cbx = this.panelDoc.getElementById("devtools-disable-cache");
    145      cbx.checked = cacheDisabled;
    146    } else if (prefName === "devtools.theme") {
    147      this.updateCurrentTheme();
    148    } else if (prefName === "devtools.source-map.client-service.enabled") {
    149      this.updateSourceMapPref();
    150    } else if (prefName === "devtools.toolbox.splitconsole.enabled") {
    151      this.toolbox.updateIsSplitConsoleEnabled();
    152    }
    153  };
    154 
    155  #themeRegistered = () => {
    156    this.setupThemeList();
    157  };
    158 
    159  #themeUnregistered = theme => {
    160    const themeBox = this.panelDoc.getElementById("devtools-theme-box");
    161    const themeInput = themeBox.querySelector(`[value=${theme.id}]`);
    162 
    163    if (themeInput) {
    164      themeInput.parentNode.remove();
    165    }
    166  };
    167 
    168  async setupToolbarButtonsList() {
    169    // Ensure the toolbox is open, and the buttons are all set up.
    170    await this.toolbox.isOpen;
    171 
    172    const enabledToolbarButtonsBox = this.panelDoc.getElementById(
    173      "enabled-toolbox-buttons-box"
    174    );
    175 
    176    const toolbarButtons = this.toolbox.toolbarButtons;
    177 
    178    if (!toolbarButtons) {
    179      console.warn("The command buttons weren't initiated yet.");
    180      return;
    181    }
    182 
    183    const onCheckboxClick = checkbox => {
    184      const commandButton = toolbarButtons.filter(
    185        toggleableButton => toggleableButton.id === checkbox.id
    186      )[0];
    187 
    188      Services.prefs.setBoolPref(
    189        commandButton.visibilityswitch,
    190        checkbox.checked
    191      );
    192      this.toolbox.updateToolboxButtonsVisibility();
    193    };
    194 
    195    const createCommandCheckbox = button => {
    196      const checkboxLabel = this.panelDoc.createElement("label");
    197      const checkboxSpanLabel = this.panelDoc.createElement("span");
    198      checkboxSpanLabel.textContent = button.description;
    199      const checkboxInput = this.panelDoc.createElement("input");
    200      checkboxInput.setAttribute("type", "checkbox");
    201      checkboxInput.setAttribute("id", button.id);
    202 
    203      if (Services.prefs.getBoolPref(button.visibilityswitch, true)) {
    204        checkboxInput.setAttribute("checked", true);
    205      }
    206      checkboxInput.addEventListener(
    207        "change",
    208        onCheckboxClick.bind(this, checkboxInput)
    209      );
    210 
    211      checkboxLabel.appendChild(checkboxInput);
    212      checkboxLabel.appendChild(checkboxSpanLabel);
    213 
    214      return checkboxLabel;
    215    };
    216 
    217    for (const button of toolbarButtons) {
    218      if (!button.isToolSupported(this.toolbox)) {
    219        continue;
    220      }
    221 
    222      enabledToolbarButtonsBox.appendChild(createCommandCheckbox(button));
    223    }
    224  }
    225 
    226  setupToolsList() {
    227    const defaultToolsBox = this.panelDoc.getElementById("default-tools-box");
    228    const additionalToolsBox = this.panelDoc.getElementById(
    229      "additional-tools-box"
    230    );
    231    const toolsNotSupportedLabel = this.panelDoc.getElementById(
    232      "tools-not-supported-label"
    233    );
    234    let atleastOneToolNotSupported = false;
    235 
    236    // Signal tool registering/unregistering globally (for the tools registered
    237    // globally) and per toolbox (for the tools registered to a single toolbox).
    238    // This event handler expect this to be binded to the related checkbox element.
    239    const onCheckboxClick = function (telemetry, tool) {
    240      // Set the kill switch pref boolean to true
    241      Services.prefs.setBoolPref(tool.visibilityswitch, this.checked);
    242 
    243      if (!tool.isWebExtension) {
    244        gDevTools.emit(
    245          this.checked ? "tool-registered" : "tool-unregistered",
    246          tool.id
    247        );
    248        // Record which tools were registered and unregistered.
    249        Glean.devtoolsTool.registered[tool.id].set(this.checked);
    250      }
    251    };
    252 
    253    const createToolCheckbox = tool => {
    254      const checkboxLabel = this.panelDoc.createElement("label");
    255      const checkboxInput = this.panelDoc.createElement("input");
    256      checkboxInput.setAttribute("type", "checkbox");
    257      checkboxInput.setAttribute("id", tool.id);
    258      checkboxInput.setAttribute("title", tool.tooltip || "");
    259 
    260      const checkboxSpanLabel = this.panelDoc.createElement("span");
    261      if (tool.isToolSupported(this.toolbox)) {
    262        checkboxSpanLabel.textContent = tool.label;
    263      } else {
    264        atleastOneToolNotSupported = true;
    265        checkboxSpanLabel.textContent = L10N.getFormatStr(
    266          "options.toolNotSupportedMarker",
    267          tool.label
    268        );
    269        checkboxInput.setAttribute("data-unsupported", "true");
    270        checkboxInput.setAttribute("disabled", "true");
    271      }
    272 
    273      if (InfallibleGetBoolPref(tool.visibilityswitch)) {
    274        checkboxInput.setAttribute("checked", "true");
    275      }
    276 
    277      checkboxInput.addEventListener(
    278        "change",
    279        onCheckboxClick.bind(checkboxInput, this.telemetry, tool)
    280      );
    281 
    282      checkboxLabel.appendChild(checkboxInput);
    283      checkboxLabel.appendChild(checkboxSpanLabel);
    284 
    285      // We shouldn't have deprecated tools anymore, but we might have one in the future,
    286      // when migrating the storage inspector to the application panel (Bug 1681059).
    287      // Let's keep this code for now so we keep the l10n property around and avoid
    288      // unnecessary translation work if we need it again in the future.
    289      if (tool.deprecated) {
    290        const deprecationURL = this.panelDoc.createElement("a");
    291        deprecationURL.title = deprecationURL.href = tool.deprecationURL;
    292        deprecationURL.textContent = L10N.getStr("options.deprecationNotice");
    293        // Cannot use a real link when we are in the Browser Toolbox.
    294        deprecationURL.addEventListener("click", e => {
    295          e.preventDefault();
    296          openDocLink(tool.deprecationURL, { relatedToCurrent: true });
    297        });
    298 
    299        const checkboxSpanDeprecated = this.panelDoc.createElement("span");
    300        checkboxSpanDeprecated.className = "deprecation-notice";
    301        checkboxLabel.appendChild(checkboxSpanDeprecated);
    302        checkboxSpanDeprecated.appendChild(deprecationURL);
    303      }
    304 
    305      return checkboxLabel;
    306    };
    307 
    308    // Clean up any existent default tools content.
    309    for (const label of defaultToolsBox.querySelectorAll("label")) {
    310      label.remove();
    311    }
    312 
    313    // Populating the default tools lists
    314    const toggleableTools = gDevTools.getDefaultTools().filter(tool => {
    315      return tool.visibilityswitch && !tool.hiddenInOptions;
    316    });
    317 
    318    const fragment = this.panelDoc.createDocumentFragment();
    319    for (const tool of toggleableTools) {
    320      fragment.appendChild(createToolCheckbox(tool));
    321    }
    322 
    323    const toolsNotSupportedLabelNode = this.panelDoc.getElementById(
    324      "tools-not-supported-label"
    325    );
    326    defaultToolsBox.insertBefore(fragment, toolsNotSupportedLabelNode);
    327 
    328    // Clean up any existent additional tools content.
    329    for (const label of additionalToolsBox.querySelectorAll("label")) {
    330      label.remove();
    331    }
    332 
    333    // Populating the additional tools list.
    334    let atleastOneAddon = false;
    335    for (const tool of gDevTools.getAdditionalTools()) {
    336      atleastOneAddon = true;
    337      additionalToolsBox.appendChild(createToolCheckbox(tool));
    338    }
    339 
    340    // Populating the additional tools that came from the installed WebExtension add-ons.
    341    for (const { uuid, name, pref } of this.toolbox.listWebExtensions()) {
    342      atleastOneAddon = true;
    343 
    344      additionalToolsBox.appendChild(
    345        createToolCheckbox({
    346          isWebExtension: true,
    347 
    348          // Use the preference as the unified webextensions tool id.
    349          id: `webext-${uuid}`,
    350          tooltip: name,
    351          label: name,
    352          // Disable the devtools extension using the given pref name:
    353          // the toolbox options for the WebExtensions are not related to a single
    354          // tool (e.g. a devtools panel created from the extension devtools_page)
    355          // but to the entire devtools part of a webextension which is enabled
    356          // by the Addon Manager (but it may be disabled by its related
    357          // devtools about:config preference), and so the following
    358          visibilityswitch: pref,
    359 
    360          // Only local tabs are currently supported as targets.
    361          isToolSupported: toolbox =>
    362            toolbox.commands.descriptorFront.isLocalTab,
    363        })
    364      );
    365    }
    366 
    367    if (!atleastOneAddon) {
    368      additionalToolsBox.style.display = "none";
    369    } else {
    370      additionalToolsBox.style.display = "";
    371    }
    372 
    373    if (!atleastOneToolNotSupported) {
    374      toolsNotSupportedLabel.style.display = "none";
    375    } else {
    376      toolsNotSupportedLabel.style.display = "";
    377    }
    378 
    379    this.panelWin.focus();
    380  }
    381 
    382  setupThemeList() {
    383    const themeBox = this.panelDoc.getElementById("devtools-theme-box");
    384    const themeLabels = themeBox.querySelectorAll("label");
    385    for (const label of themeLabels) {
    386      label.remove();
    387    }
    388 
    389    const createThemeOption = theme => {
    390      const inputLabel = this.panelDoc.createElement("label");
    391      const inputRadio = this.panelDoc.createElement("input");
    392      inputRadio.setAttribute("type", "radio");
    393      inputRadio.setAttribute("value", theme.id);
    394      inputRadio.setAttribute("name", "devtools-theme-item");
    395      inputRadio.addEventListener("change", function (e) {
    396        SetPref(themeBox.getAttribute("data-pref"), e.target.value);
    397      });
    398 
    399      const inputSpanLabel = this.panelDoc.createElement("span");
    400      inputSpanLabel.textContent = theme.label;
    401      inputLabel.appendChild(inputRadio);
    402      inputLabel.appendChild(inputSpanLabel);
    403 
    404      return inputLabel;
    405    };
    406 
    407    // Populating the default theme list
    408    themeBox.appendChild(
    409      createThemeOption({
    410        id: "auto",
    411        label: L10N.getStr("options.autoTheme.label"),
    412      })
    413    );
    414 
    415    const themes = gDevTools.getThemeDefinitionArray();
    416    for (const theme of themes) {
    417      themeBox.appendChild(createThemeOption(theme));
    418    }
    419 
    420    this.updateCurrentTheme();
    421  }
    422 
    423  /**
    424   * Add extra checkbox options bound to a boolean preference.
    425   */
    426  setupAdditionalOptions() {
    427    const prefDefinitions = [
    428      {
    429        pref: "devtools.custom-formatters.enabled",
    430        l10nLabelId: "options-enable-custom-formatters-label",
    431        l10nTooltipId: "options-enable-custom-formatters-tooltip",
    432        id: "devtools-custom-formatters",
    433        parentId: "context-options",
    434      },
    435    ];
    436 
    437    const createPreferenceOption = ({
    438      pref,
    439      label,
    440      l10nLabelId,
    441      l10nTooltipId,
    442      id,
    443      onChange,
    444    }) => {
    445      const inputLabel = this.panelDoc.createElement("label");
    446      if (l10nTooltipId) {
    447        this.panelDoc.l10n.setAttributes(inputLabel, l10nTooltipId);
    448      }
    449      const checkbox = this.panelDoc.createElement("input");
    450      checkbox.setAttribute("type", "checkbox");
    451      if (GetPref(pref)) {
    452        checkbox.setAttribute("checked", "checked");
    453      }
    454      checkbox.setAttribute("id", id);
    455      checkbox.addEventListener("change", e => {
    456        SetPref(pref, e.target.checked);
    457        if (onChange) {
    458          onChange(e.target.checked);
    459        }
    460      });
    461 
    462      const inputSpanLabel = this.panelDoc.createElement("span");
    463      if (l10nLabelId) {
    464        this.panelDoc.l10n.setAttributes(inputSpanLabel, l10nLabelId);
    465      } else if (label) {
    466        inputSpanLabel.textContent = label;
    467      }
    468      inputLabel.appendChild(checkbox);
    469      inputLabel.appendChild(inputSpanLabel);
    470 
    471      return inputLabel;
    472    };
    473 
    474    for (const prefDefinition of prefDefinitions) {
    475      const parent = this.panelDoc.getElementById(prefDefinition.parentId);
    476      // We want to insert the new definition after the last existing
    477      // definition, but before any other element.
    478      // For example in the "Advanced Settings" column there's indeed a <span>
    479      // text at the end, and we want that it stays at the end.
    480      // The reference element can be `null` if there's no label or if there's
    481      // no element after the last label. But that's OK and it will do what we
    482      // want.
    483      const referenceElement = parent.querySelector("label:last-of-type + *");
    484      parent.insertBefore(
    485        createPreferenceOption(prefDefinition),
    486        referenceElement
    487      );
    488    }
    489  }
    490 
    491  async populatePreferences() {
    492    const prefCheckboxes = this.panelDoc.querySelectorAll(
    493      "input[type=checkbox][data-pref]"
    494    );
    495    for (const prefCheckbox of prefCheckboxes) {
    496      if (GetPref(prefCheckbox.getAttribute("data-pref"))) {
    497        prefCheckbox.setAttribute("checked", true);
    498      }
    499      prefCheckbox.addEventListener("change", e => {
    500        const checkbox = e.target;
    501        SetPref(checkbox.getAttribute("data-pref"), checkbox.checked);
    502        if (checkbox.hasAttribute("data-force-reload")) {
    503          this.commands.targetCommand.reloadTopLevelTarget();
    504        }
    505      });
    506    }
    507    // Themes radio inputs are handled in setupThemeList
    508    const prefRadiogroups = this.panelDoc.querySelectorAll(
    509      ".radiogroup[data-pref]:not(#devtools-theme-box)"
    510    );
    511    for (const radioGroup of prefRadiogroups) {
    512      const selectedValue = GetPref(radioGroup.getAttribute("data-pref"));
    513 
    514      for (const radioInput of radioGroup.querySelectorAll(
    515        "input[type=radio]"
    516      )) {
    517        if (radioInput.getAttribute("value") == selectedValue) {
    518          radioInput.setAttribute("checked", true);
    519        }
    520 
    521        radioInput.addEventListener("change", function (e) {
    522          SetPref(radioGroup.getAttribute("data-pref"), e.target.value);
    523        });
    524      }
    525    }
    526    const prefSelects = this.panelDoc.querySelectorAll("select[data-pref]");
    527    for (const prefSelect of prefSelects) {
    528      const pref = GetPref(prefSelect.getAttribute("data-pref"));
    529      const options = [...prefSelect.options];
    530      options.some(function (option) {
    531        const value = option.value;
    532        // non strict check to allow int values.
    533        if (value == pref) {
    534          prefSelect.selectedIndex = options.indexOf(option);
    535          return true;
    536        }
    537        return false;
    538      });
    539 
    540      prefSelect.addEventListener("change", function (e) {
    541        const select = e.target;
    542        SetPref(
    543          select.getAttribute("data-pref"),
    544          select.options[select.selectedIndex].value
    545        );
    546      });
    547    }
    548 
    549    if (this.commands.descriptorFront.isTabDescriptor) {
    550      const isJavascriptEnabled =
    551        await this.commands.targetConfigurationCommand.isJavascriptEnabled();
    552      this.disableJSNode.checked = !isJavascriptEnabled;
    553      this.disableJSNode.addEventListener("click", this.#disableJSClicked);
    554    } else {
    555      // Hide the checkbox and label
    556      this.disableJSNode.parentNode.style.display = "none";
    557    }
    558  }
    559 
    560  updateCurrentTheme() {
    561    const currentTheme = GetPref("devtools.theme");
    562    const themeBox = this.panelDoc.getElementById("devtools-theme-box");
    563    const themeRadioInput = themeBox.querySelector(`[value=${currentTheme}]`);
    564 
    565    if (themeRadioInput) {
    566      themeRadioInput.checked = true;
    567    } else {
    568      // If the current theme does not exist anymore, switch to auto theme
    569      const autoThemeInputRadio = themeBox.querySelector("[value=auto]");
    570      autoThemeInputRadio.checked = true;
    571    }
    572  }
    573 
    574  updateSourceMapPref() {
    575    const prefName = "devtools.source-map.client-service.enabled";
    576    const enabled = GetPref(prefName);
    577    const box = this.panelDoc.querySelector(`[data-pref="${prefName}"]`);
    578    box.checked = enabled;
    579  }
    580 
    581  /**
    582   * Disables JavaScript for the currently loaded tab. We force a page refresh
    583   * here because setting browsingContext.allowJavascript to true fails to block
    584   * JS execution from event listeners added using addEventListener(), AJAX
    585   * calls and timers. The page refresh prevents these things from being added
    586   * in the first place.
    587   *
    588   * @param {Event} event
    589   *        The event sent by checking / unchecking the disable JS checkbox.
    590   */
    591  #disableJSClicked = event => {
    592    const checked = event.target.checked;
    593 
    594    this.commands.targetConfigurationCommand.updateConfiguration({
    595      javascriptEnabled: !checked,
    596    });
    597  };
    598 
    599  destroy() {
    600    if (this.destroyed) {
    601      return;
    602    }
    603    this.destroyed = true;
    604 
    605    this.#removeListeners();
    606 
    607    this.disableJSNode.removeEventListener("click", this.#disableJSClicked);
    608 
    609    this.panelWin = this.panelDoc = this.disableJSNode = this.toolbox = null;
    610  }
    611 }
    612 
    613 exports.OptionsPanel = OptionsPanel;