tor-browser

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

ext-devtools.js (16256B)


      1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* vim: set sts=2 sw=2 et tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 "use strict";
      8 
      9 /**
     10 * This module provides helpers used by the other specialized `ext-devtools-*.js` modules
     11 * and the implementation of the `devtools_page`.
     12 */
     13 
     14 ChromeUtils.defineESModuleGetters(this, {
     15  DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
     16 });
     17 
     18 var { ExtensionParent } = ChromeUtils.importESModule(
     19  "resource://gre/modules/ExtensionParent.sys.mjs"
     20 );
     21 
     22 var { HiddenExtensionPage, watchExtensionProxyContextLoad } = ExtensionParent;
     23 
     24 // Get the devtools preference given the extension id.
     25 function getDevToolsPrefBranchName(extensionId) {
     26  return `devtools.webextensions.${extensionId}`;
     27 }
     28 
     29 /**
     30 * Retrieve the tabId for the given devtools toolbox.
     31 *
     32 * @param {Toolbox} toolbox
     33 *   A devtools toolbox instance.
     34 *
     35 * @returns {number}
     36 *   The corresponding WebExtensions tabId.
     37 */
     38 global.getTargetTabIdForToolbox = toolbox => {
     39  let { descriptorFront } = toolbox.commands;
     40 
     41  if (!descriptorFront.isLocalTab) {
     42    throw new Error(
     43      "Unexpected target type: only local tabs are currently supported."
     44    );
     45  }
     46 
     47  let parentWindow = descriptorFront.localTab.linkedBrowser.ownerGlobal;
     48  let tab = parentWindow.gBrowser.getTabForBrowser(
     49    descriptorFront.localTab.linkedBrowser
     50  );
     51 
     52  return tabTracker.getId(tab);
     53 };
     54 
     55 // Get the WebExtensionInspectedWindowActor eval options (needed to provide the $0 and inspect
     56 // binding provided to the evaluated js code).
     57 global.getToolboxEvalOptions = async function (context) {
     58  const options = {};
     59  const toolbox = context.devToolsToolbox;
     60  const selectedNode = toolbox.selection;
     61 
     62  if (selectedNode && selectedNode.nodeFront) {
     63    // If there is a selected node in the inspector, we hand over
     64    // its actor id to the eval request in order to provide the "$0" binding.
     65    options.toolboxSelectedNodeActorID = selectedNode.nodeFront.actorID;
     66  }
     67 
     68  // Provide the console actor ID to implement the "inspect" binding.
     69  const consoleFront = await toolbox.target.getFront("console");
     70  options.toolboxConsoleActorID = consoleFront.actor;
     71 
     72  return options;
     73 };
     74 
     75 /**
     76 * The DevToolsPage represents the "devtools_page" related to a particular
     77 * Toolbox and WebExtension.
     78 *
     79 * The devtools_page contexts are invisible WebExtensions contexts, similar to the
     80 * background page, associated to a single developer toolbox (e.g. If an add-on
     81 * registers a devtools_page and the user opens 3 developer toolbox in 3 webpages,
     82 * 3 devtools_page contexts will be created for that add-on).
     83 *
     84 * @param {Extension}              extension
     85 *   The extension that owns the devtools_page.
     86 * @param {object}                 options
     87 * @param {Toolbox}                options.toolbox
     88 *   The developer toolbox instance related to this devtools_page.
     89 * @param {string}                 options.url
     90 *   The path to the devtools page html page relative to the extension base URL.
     91 * @param {DevToolsPageDefinition} options.devToolsPageDefinition
     92 *   The instance of the devToolsPageDefinition class related to this DevToolsPage.
     93 */
     94 class DevToolsPage extends HiddenExtensionPage {
     95  constructor(extension, options) {
     96    super(extension, "devtools_page");
     97 
     98    this.url = extension.baseURI.resolve(options.url);
     99    this.toolbox = options.toolbox;
    100    this.devToolsPageDefinition = options.devToolsPageDefinition;
    101 
    102    this.unwatchExtensionProxyContextLoad = null;
    103 
    104    this.waitForTopLevelContext = new Promise(resolve => {
    105      this.resolveTopLevelContext = resolve;
    106    });
    107  }
    108 
    109  async build() {
    110    await this.createBrowserElement();
    111 
    112    // Listening to new proxy contexts.
    113    this.unwatchExtensionProxyContextLoad = watchExtensionProxyContextLoad(
    114      this,
    115      context => {
    116        // Keep track of the toolbox and target associated to the context, which is
    117        // needed by the API methods implementation.
    118        context.devToolsToolbox = this.toolbox;
    119 
    120        if (!this.topLevelContext) {
    121          this.topLevelContext = context;
    122 
    123          // Ensure this devtools page is destroyed, when the top level context proxy is
    124          // closed.
    125          this.topLevelContext.callOnClose(this);
    126 
    127          this.resolveTopLevelContext(context);
    128        }
    129      }
    130    );
    131 
    132    extensions.emit("extension-browser-inserted", this.browser, {
    133      devtoolsToolboxInfo: {
    134        inspectedWindowTabId: getTargetTabIdForToolbox(this.toolbox),
    135        themeName: DevToolsShim.getTheme(),
    136      },
    137    });
    138 
    139    this.browser.fixupAndLoadURIString(this.url, {
    140      triggeringPrincipal: this.extension.principal,
    141    });
    142 
    143    await this.waitForTopLevelContext;
    144  }
    145 
    146  close() {
    147    if (this.closed) {
    148      throw new Error("Unable to shutdown a closed DevToolsPage instance");
    149    }
    150 
    151    this.closed = true;
    152 
    153    // Unregister the devtools page instance from the devtools page definition.
    154    this.devToolsPageDefinition.forgetForToolbox(this.toolbox);
    155 
    156    // Unregister it from the resources to cleanup when the context has been closed.
    157    if (this.topLevelContext) {
    158      this.topLevelContext.forgetOnClose(this);
    159    }
    160 
    161    // Stop watching for any new proxy contexts from the devtools page.
    162    if (this.unwatchExtensionProxyContextLoad) {
    163      this.unwatchExtensionProxyContextLoad();
    164      this.unwatchExtensionProxyContextLoad = null;
    165    }
    166 
    167    super.shutdown();
    168  }
    169 }
    170 
    171 /**
    172 * The DevToolsPageDefinitions class represents the "devtools_page" manifest property
    173 * of a WebExtension.
    174 *
    175 * A DevToolsPageDefinition instance is created automatically when a WebExtension
    176 * which contains the "devtools_page" manifest property has been loaded, and it is
    177 * automatically destroyed when the related WebExtension has been unloaded,
    178 * and so there will be at most one DevtoolsPageDefinition per add-on.
    179 *
    180 * Every time a developer tools toolbox is opened, the DevToolsPageDefinition creates
    181 * and keep track of a DevToolsPage instance (which represents the actual devtools_page
    182 * instance related to that particular toolbox).
    183 *
    184 * @param {Extension} extension
    185 *   The extension that owns the devtools_page.
    186 * @param {string}    url
    187 *   The path to the devtools page html page relative to the extension base URL.
    188 */
    189 class DevToolsPageDefinition {
    190  constructor(extension, url) {
    191    this.url = url;
    192    this.extension = extension;
    193 
    194    // Map[Toolbox -> DevToolsPage]
    195    this.devtoolsPageForToolbox = new Map();
    196  }
    197 
    198  onThemeChanged(themeName) {
    199    Services.ppmm.broadcastAsyncMessage("Extension:DevToolsThemeChanged", {
    200      themeName,
    201    });
    202  }
    203 
    204  buildForToolbox(toolbox) {
    205    if (
    206      !this.extension.canAccessWindow(
    207        toolbox.commands.descriptorFront.localTab.ownerGlobal
    208      )
    209    ) {
    210      // We should never create a devtools page for a toolbox related to a private browsing window
    211      // if the extension is not allowed to access it.
    212      return;
    213    }
    214 
    215    if (this.devtoolsPageForToolbox.has(toolbox)) {
    216      return Promise.reject(
    217        new Error("DevtoolsPage has been already created for this toolbox")
    218      );
    219    }
    220 
    221    const devtoolsPage = new DevToolsPage(this.extension, {
    222      toolbox,
    223      url: this.url,
    224      devToolsPageDefinition: this,
    225    });
    226 
    227    // If this is the first DevToolsPage, subscribe to the theme-changed event
    228    if (this.devtoolsPageForToolbox.size === 0) {
    229      DevToolsShim.on("theme-changed", this.onThemeChanged);
    230    }
    231    this.devtoolsPageForToolbox.set(toolbox, devtoolsPage);
    232 
    233    return devtoolsPage.build();
    234  }
    235 
    236  shutdownForToolbox(toolbox) {
    237    if (this.devtoolsPageForToolbox.has(toolbox)) {
    238      const devtoolsPage = this.devtoolsPageForToolbox.get(toolbox);
    239      devtoolsPage.close();
    240 
    241      // `devtoolsPage.close()` should remove the instance from the map,
    242      // raise an exception if it is still there.
    243      if (this.devtoolsPageForToolbox.has(toolbox)) {
    244        throw new Error(
    245          `Leaked DevToolsPage instance for target "${toolbox.commands.descriptorFront.url}", extension "${this.extension.policy.debugName}"`
    246        );
    247      }
    248 
    249      // If this was the last DevToolsPage, unsubscribe from the theme-changed event
    250      if (this.devtoolsPageForToolbox.size === 0) {
    251        DevToolsShim.off("theme-changed", this.onThemeChanged);
    252      }
    253      this.extension.emit("devtools-page-shutdown", toolbox);
    254    }
    255  }
    256 
    257  forgetForToolbox(toolbox) {
    258    this.devtoolsPageForToolbox.delete(toolbox);
    259  }
    260 
    261  /**
    262   * Build the devtools_page instances for all the existing toolboxes
    263   * (if the toolbox target is supported).
    264   */
    265  build() {
    266    // Iterate over the existing toolboxes and create the devtools page for them
    267    // (if the toolbox target is supported).
    268    for (let toolbox of DevToolsShim.getToolboxes()) {
    269      if (
    270        // Skip toolboxes in the middle of their destroy sequence (fully
    271        // destroyed will not be returned by getToolboxes()).
    272        toolbox.isDestroying() ||
    273        // Skip remote / non-web toolboxes (ie. target is not a local tab).
    274        !toolbox.commands.descriptorFront.isLocalTab ||
    275        // Skip private browsing windows if the extension is not allowed to
    276        // access them.
    277        !this.extension.canAccessWindow(
    278          toolbox.commands.descriptorFront.localTab.ownerGlobal
    279        )
    280      ) {
    281        continue;
    282      }
    283 
    284      // Ensure that the WebExtension is listed in the toolbox options.
    285      toolbox.registerWebExtension(this.extension.uuid, {
    286        name: this.extension.name,
    287        pref: `${getDevToolsPrefBranchName(this.extension.id)}.enabled`,
    288      });
    289 
    290      this.buildForToolbox(toolbox);
    291    }
    292  }
    293 
    294  /**
    295   * Shutdown all the devtools_page instances.
    296   */
    297  shutdown() {
    298    for (let toolbox of this.devtoolsPageForToolbox.keys()) {
    299      this.shutdownForToolbox(toolbox);
    300    }
    301 
    302    if (this.devtoolsPageForToolbox.size > 0) {
    303      throw new Error(
    304        `Leaked ${this.devtoolsPageForToolbox.size} DevToolsPage instances in devtoolsPageForToolbox Map`
    305      );
    306    }
    307  }
    308 }
    309 
    310 this.devtools = class extends ExtensionAPI {
    311  constructor(extension) {
    312    super(extension);
    313 
    314    this._initialized = false;
    315 
    316    // DevToolsPageDefinition instance (created in onManifestEntry).
    317    this.pageDefinition = null;
    318 
    319    this.onToolboxReady = this.onToolboxReady.bind(this);
    320    this.onToolboxDestroy = this.onToolboxDestroy.bind(this);
    321 
    322    /* eslint-disable mozilla/balanced-listeners */
    323    extension.on("add-permissions", (ignoreEvent, permissions) => {
    324      if (permissions.permissions.includes("devtools")) {
    325        Services.prefs.setBoolPref(
    326          `${getDevToolsPrefBranchName(extension.id)}.enabled`,
    327          true
    328        );
    329 
    330        this._initialize();
    331      }
    332    });
    333 
    334    extension.on("remove-permissions", (ignoreEvent, permissions) => {
    335      if (permissions.permissions.includes("devtools")) {
    336        Services.prefs.setBoolPref(
    337          `${getDevToolsPrefBranchName(extension.id)}.enabled`,
    338          false
    339        );
    340 
    341        this._uninitialize();
    342      }
    343    });
    344  }
    345 
    346  onManifestEntry() {
    347    this._initialize();
    348  }
    349 
    350  static onUninstall(extensionId) {
    351    // Remove the preference branch on uninstall.
    352    const prefBranch = Services.prefs.getBranch(
    353      `${getDevToolsPrefBranchName(extensionId)}.`
    354    );
    355 
    356    prefBranch.deleteBranch("");
    357  }
    358 
    359  _initialize() {
    360    const { extension } = this;
    361 
    362    if (!extension.hasPermission("devtools") || this._initialized) {
    363      return;
    364    }
    365 
    366    this.initDevToolsPref();
    367 
    368    // Create the devtools_page definition.
    369    this.pageDefinition = new DevToolsPageDefinition(
    370      extension,
    371      extension.manifest.devtools_page
    372    );
    373 
    374    // Build the extension devtools_page on all existing toolboxes (if the extension
    375    // devtools_page is not disabled by the related preference).
    376    if (!this.isDevToolsPageDisabled()) {
    377      this.pageDefinition.build();
    378    }
    379 
    380    DevToolsShim.on("toolbox-ready", this.onToolboxReady);
    381    DevToolsShim.on("toolbox-destroy", this.onToolboxDestroy);
    382    this._initialized = true;
    383  }
    384 
    385  _uninitialize() {
    386    // devtoolsPrefBranch is set in onManifestEntry, and nullified
    387    // later in onShutdown.  If it isn't set, then onManifestEntry
    388    // did not initialize devtools for the extension.
    389    if (!this._initialized) {
    390      return;
    391    }
    392 
    393    DevToolsShim.off("toolbox-ready", this.onToolboxReady);
    394    DevToolsShim.off("toolbox-destroy", this.onToolboxDestroy);
    395 
    396    // Shutdown the extension devtools_page from all existing toolboxes.
    397    this.pageDefinition.shutdown();
    398    this.pageDefinition = null;
    399 
    400    // Iterate over the existing toolboxes and unlist the devtools webextension from them.
    401    for (let toolbox of DevToolsShim.getToolboxes()) {
    402      toolbox.unregisterWebExtension(this.extension.uuid);
    403    }
    404 
    405    this.uninitDevToolsPref();
    406    this._initialized = false;
    407  }
    408 
    409  onShutdown() {
    410    this._uninitialize();
    411  }
    412 
    413  getAPI() {
    414    return {
    415      devtools: {},
    416    };
    417  }
    418 
    419  onToolboxReady(toolbox) {
    420    if (
    421      !toolbox.commands.descriptorFront.isLocalTab ||
    422      !this.extension.canAccessWindow(
    423        toolbox.commands.descriptorFront.localTab.ownerGlobal
    424      )
    425    ) {
    426      // Skip any non-local (as remote tabs are not yet supported, see Bug 1304378 for additional details
    427      // related to remote targets support), and private browsing windows if the extension
    428      // is not allowed to access them.
    429      return;
    430    }
    431 
    432    // Ensure that the WebExtension is listed in the toolbox options.
    433    toolbox.registerWebExtension(this.extension.uuid, {
    434      name: this.extension.name,
    435      pref: `${getDevToolsPrefBranchName(this.extension.id)}.enabled`,
    436    });
    437 
    438    // Do not build the devtools page if the extension has been disabled
    439    // (e.g. based on the devtools preference).
    440    if (toolbox.isWebExtensionEnabled(this.extension.uuid)) {
    441      this.pageDefinition.buildForToolbox(toolbox);
    442    }
    443  }
    444 
    445  onToolboxDestroy(toolbox) {
    446    if (!toolbox.commands.descriptorFront.isLocalTab) {
    447      // Only local tabs are currently supported (See Bug 1304378 for additional details
    448      // related to remote targets support).
    449      return;
    450    }
    451 
    452    this.pageDefinition.shutdownForToolbox(toolbox);
    453  }
    454 
    455  /**
    456   * Initialize the DevTools preferences branch for the extension and
    457   * start to observe it for changes on the "enabled" preference.
    458   */
    459  initDevToolsPref() {
    460    const prefBranch = Services.prefs.getBranch(
    461      `${getDevToolsPrefBranchName(this.extension.id)}.`
    462    );
    463 
    464    // Initialize the devtools extension preference if it doesn't exist yet.
    465    if (prefBranch.getPrefType("enabled") === prefBranch.PREF_INVALID) {
    466      prefBranch.setBoolPref("enabled", true);
    467    }
    468 
    469    this.devtoolsPrefBranch = prefBranch;
    470    this.devtoolsPrefBranch.addObserver("enabled", this);
    471  }
    472 
    473  /**
    474   * Stop from observing the DevTools preferences branch for the extension.
    475   */
    476  uninitDevToolsPref() {
    477    this.devtoolsPrefBranch.removeObserver("enabled", this);
    478    this.devtoolsPrefBranch = null;
    479  }
    480 
    481  /**
    482   * Test if the extension's devtools_page has been disabled using the
    483   * DevTools preference.
    484   *
    485   * @returns {boolean}
    486   *          true if the devtools_page for this extension is disabled.
    487   */
    488  isDevToolsPageDisabled() {
    489    return !this.devtoolsPrefBranch.getBoolPref("enabled", false);
    490  }
    491 
    492  /**
    493   * Observes the changed preferences on the DevTools preferences branch
    494   * related to the extension.
    495   *
    496   * @param {nsIPrefBranch} subject  The observed preferences branch.
    497   * @param {string}        topic    The notified topic.
    498   * @param {string}        prefName The changed preference name.
    499   */
    500  observe(subject, topic, prefName) {
    501    // We are currently interested only in the "enabled" preference from the
    502    // WebExtension devtools preferences branch.
    503    if (subject !== this.devtoolsPrefBranch || prefName !== "enabled") {
    504      return;
    505    }
    506 
    507    // Shutdown or build the devtools_page on any existing toolbox.
    508    if (this.isDevToolsPageDisabled()) {
    509      this.pageDefinition.shutdown();
    510    } else {
    511      this.pageDefinition.build();
    512    }
    513  }
    514 };