tor-browser

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

ext-devtools-panels.js (9559B)


      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 var { XPCOMUtils } = ChromeUtils.importESModule(
     10  "resource://gre/modules/XPCOMUtils.sys.mjs"
     11 );
     12 
     13 XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
     14 
     15 ChromeUtils.defineESModuleGetters(this, {
     16  ExtensionChildDevToolsUtils:
     17    "resource://gre/modules/ExtensionChildDevToolsUtils.sys.mjs",
     18 });
     19 
     20 var { promiseDocumentLoaded } = ExtensionUtils;
     21 
     22 /**
     23 * Represents an addon devtools panel in the child process.
     24 *
     25 * @param {DevtoolsExtensionContext}
     26 *   A devtools extension context running in a child process.
     27 * @param {object} panelOptions
     28 * @param {string} panelOptions.id
     29 *   The id of the addon devtools panel registered in the main process.
     30 */
     31 class ChildDevToolsPanel extends ExtensionCommon.EventEmitter {
     32  constructor(context, { id }) {
     33    super();
     34 
     35    this.context = context;
     36    this.context.callOnClose(this);
     37 
     38    this.id = id;
     39    this._panelContext = null;
     40 
     41    this.conduit = context.openConduit(this, {
     42      recv: ["PanelHidden", "PanelShown"],
     43    });
     44  }
     45 
     46  get panelContext() {
     47    if (this._panelContext) {
     48      return this._panelContext;
     49    }
     50 
     51    for (let view of this.context.extension.devtoolsViews) {
     52      if (
     53        view.viewType === "devtools_panel" &&
     54        view.devtoolsToolboxInfo.toolboxPanelId === this.id
     55      ) {
     56        this._panelContext = view;
     57 
     58        // Reset the cached _panelContext property when the view is closed.
     59        view.callOnClose({
     60          close: () => {
     61            this._panelContext = null;
     62          },
     63        });
     64        return view;
     65      }
     66    }
     67 
     68    return null;
     69  }
     70 
     71  recvPanelShown() {
     72    // Ignore received call before the panel context exist.
     73    if (!this.panelContext || !this.panelContext.contentWindow) {
     74      return;
     75    }
     76    const { document } = this.panelContext.contentWindow;
     77 
     78    // Ensure that the onShown event is fired when the panel document has
     79    // been fully loaded.
     80    promiseDocumentLoaded(document).then(() => {
     81      this.emit("shown", this.panelContext.contentWindow);
     82    });
     83  }
     84 
     85  recvPanelHidden() {
     86    this.emit("hidden");
     87  }
     88 
     89  api() {
     90    return {
     91      onShown: new EventManager({
     92        context: this.context,
     93        name: "devtoolsPanel.onShown",
     94        register: fire => {
     95          const listener = (eventName, panelContentWindow) => {
     96            fire.asyncWithoutClone(panelContentWindow);
     97          };
     98          this.on("shown", listener);
     99          return () => {
    100            this.off("shown", listener);
    101          };
    102        },
    103      }).api(),
    104 
    105      onHidden: new EventManager({
    106        context: this.context,
    107        name: "devtoolsPanel.onHidden",
    108        register: fire => {
    109          const listener = () => {
    110            fire.async();
    111          };
    112          this.on("hidden", listener);
    113          return () => {
    114            this.off("hidden", listener);
    115          };
    116        },
    117      }).api(),
    118 
    119      // TODO(rpl): onSearch event and createStatusBarButton method
    120    };
    121  }
    122 
    123  close() {
    124    this._panelContext = null;
    125    this.context = null;
    126  }
    127 }
    128 
    129 /**
    130 * Represents an addon devtools inspector sidebar in the child process.
    131 *
    132 * @param {DevtoolsExtensionContext}
    133 *   A devtools extension context running in a child process.
    134 * @param {object} sidebarOptions
    135 * @param {string} sidebarOptions.id
    136 *   The id of the addon devtools sidebar registered in the main process.
    137 */
    138 class ChildDevToolsInspectorSidebar extends ExtensionCommon.EventEmitter {
    139  constructor(context, { id }) {
    140    super();
    141 
    142    this.context = context;
    143    this.context.callOnClose(this);
    144 
    145    this.id = id;
    146 
    147    this.conduit = context.openConduit(this, {
    148      recv: ["InspectorSidebarHidden", "InspectorSidebarShown"],
    149    });
    150  }
    151 
    152  close() {
    153    this.context = null;
    154  }
    155 
    156  recvInspectorSidebarShown() {
    157    // TODO: wait and emit sidebar contentWindow once sidebar.setPage is supported.
    158    this.emit("shown");
    159  }
    160 
    161  recvInspectorSidebarHidden() {
    162    this.emit("hidden");
    163  }
    164 
    165  api() {
    166    const { context, id } = this;
    167 
    168    let extensionURL = new URL("/", context.uri.spec);
    169 
    170    // This is currently needed by sidebar.setPage because API objects are not automatically wrapped
    171    // by the API Schema validations and so the ExtensionURL type used in the JSON schema
    172    // doesn't have any effect on the parameter received by the setPage API method.
    173    function resolveExtensionURL(url) {
    174      let sidebarPageURL = new URL(url, context.uri.spec);
    175 
    176      if (
    177        extensionURL.protocol !== sidebarPageURL.protocol ||
    178        extensionURL.host !== sidebarPageURL.host
    179      ) {
    180        throw new context.cloneScope.Error(
    181          `Invalid sidebar URL: ${sidebarPageURL.href} is not a valid extension URL`
    182        );
    183      }
    184 
    185      return sidebarPageURL.href;
    186    }
    187 
    188    return {
    189      onShown: new EventManager({
    190        context,
    191        name: "devtoolsInspectorSidebar.onShown",
    192        register: fire => {
    193          const listener = (eventName, panelContentWindow) => {
    194            fire.asyncWithoutClone(panelContentWindow);
    195          };
    196          this.on("shown", listener);
    197          return () => {
    198            this.off("shown", listener);
    199          };
    200        },
    201      }).api(),
    202 
    203      onHidden: new EventManager({
    204        context,
    205        name: "devtoolsInspectorSidebar.onHidden",
    206        register: fire => {
    207          const listener = () => {
    208            fire.async();
    209          };
    210          this.on("hidden", listener);
    211          return () => {
    212            this.off("hidden", listener);
    213          };
    214        },
    215      }).api(),
    216 
    217      setPage(extensionPageURL) {
    218        let resolvedSidebarURL = resolveExtensionURL(extensionPageURL);
    219 
    220        return context.childManager.callParentAsyncFunction(
    221          "devtools.panels.elements.Sidebar.setPage",
    222          [id, resolvedSidebarURL]
    223        );
    224      },
    225 
    226      setObject(jsonObject, rootTitle) {
    227        return context.cloneScope.Promise.resolve().then(() => {
    228          return context.childManager.callParentAsyncFunction(
    229            "devtools.panels.elements.Sidebar.setObject",
    230            [id, jsonObject, rootTitle]
    231          );
    232        });
    233      },
    234 
    235      setExpression(evalExpression, rootTitle) {
    236        return context.cloneScope.Promise.resolve().then(() => {
    237          return context.childManager.callParentAsyncFunction(
    238            "devtools.panels.elements.Sidebar.setExpression",
    239            [id, evalExpression, rootTitle]
    240          );
    241        });
    242      },
    243    };
    244  }
    245 }
    246 
    247 this.devtools_panels = class extends ExtensionAPI {
    248  getAPI(context) {
    249    const themeChangeObserver =
    250      ExtensionChildDevToolsUtils.getThemeChangeObserver();
    251 
    252    return {
    253      devtools: {
    254        panels: {
    255          elements: {
    256            createSidebarPane(title) {
    257              // NOTE: this is needed to be able to return to the caller (the extension)
    258              // a promise object that it had the privileges to use (e.g. by marking this
    259              // method async we will return a promise object which can only be used by
    260              // chrome privileged code).
    261              return context.cloneScope.Promise.resolve().then(async () => {
    262                const sidebarId =
    263                  await context.childManager.callParentAsyncFunction(
    264                    "devtools.panels.elements.createSidebarPane",
    265                    [title]
    266                  );
    267 
    268                const sidebar = new ChildDevToolsInspectorSidebar(context, {
    269                  id: sidebarId,
    270                });
    271 
    272                const sidebarAPI = Cu.cloneInto(
    273                  sidebar.api(),
    274                  context.cloneScope,
    275                  { cloneFunctions: true }
    276                );
    277 
    278                return sidebarAPI;
    279              });
    280            },
    281          },
    282          create(title, icon, url) {
    283            // NOTE: this is needed to be able to return to the caller (the extension)
    284            // a promise object that it had the privileges to use (e.g. by marking this
    285            // method async we will return a promise object which can only be used by
    286            // chrome privileged code).
    287            return context.cloneScope.Promise.resolve().then(async () => {
    288              const panelId =
    289                await context.childManager.callParentAsyncFunction(
    290                  "devtools.panels.create",
    291                  [title, icon, url]
    292                );
    293 
    294              const devtoolsPanel = new ChildDevToolsPanel(context, {
    295                id: panelId,
    296              });
    297 
    298              const devtoolsPanelAPI = Cu.cloneInto(
    299                devtoolsPanel.api(),
    300                context.cloneScope,
    301                { cloneFunctions: true }
    302              );
    303              return devtoolsPanelAPI;
    304            });
    305          },
    306          get themeName() {
    307            return themeChangeObserver.themeName;
    308          },
    309          onThemeChanged: new EventManager({
    310            context,
    311            name: "devtools.panels.onThemeChanged",
    312            register: fire => {
    313              const listener = (eventName, themeName) => {
    314                fire.async(themeName);
    315              };
    316              themeChangeObserver.on("themeChanged", listener);
    317              return () => {
    318                themeChangeObserver.off("themeChanged", listener);
    319              };
    320            },
    321          }).api(),
    322        },
    323      },
    324    };
    325  }
    326 };