tor-browser

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

toolbox-hosts.js (13731B)


      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 
      9 loader.lazyRequireGetter(
     10  this,
     11  "gDevToolsBrowser",
     12  "resource://devtools/client/framework/devtools-browser.js",
     13  true
     14 );
     15 
     16 const lazy = {};
     17 ChromeUtils.defineESModuleGetters(lazy, {
     18  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     19 });
     20 
     21 /* A host should always allow this much space for the page to be displayed.
     22 * There is also a min-height on the browser, but we still don't want to set
     23 * frame.style.height to be larger than that, since it can cause problems with
     24 * resizing the toolbox and panel layout. */
     25 const MIN_PAGE_SIZE = 25;
     26 
     27 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
     28 
     29 /**
     30 * A toolbox host represents an object that contains a toolbox (e.g. the
     31 * sidebar or a separate window). Any host object should implement the
     32 * following functions:
     33 *
     34 * create() - create the UI
     35 * raise() - bring UI in foreground
     36 * setTitle - update UI's visible title (if any)
     37 * destroy() - destroy the host's UI
     38 */
     39 
     40 /**
     41 * Base class for any in-browser host: left, bottom, right.
     42 */
     43 class BaseInBrowserHost {
     44  /**
     45   * @param {Tab} hostTab
     46   *              The web page's tab where DevTools are displayed.
     47   * @param {string} type
     48   *              The host type: left, bottom, right.
     49   */
     50  constructor(hostTab, type) {
     51    this.hostTab = hostTab;
     52    this.type = type;
     53 
     54    this._gBrowser = this.hostTab.ownerGlobal.gBrowser;
     55    this._browserContainer = this._gBrowser.getBrowserContainer(
     56      this.hostTab.linkedBrowser
     57    );
     58 
     59    // Reference to the <browser> element used to load DevTools.
     60    // This is created by each subclass from create() method
     61    this.frame = null;
     62 
     63    Services.obs.addObserver(this, "browsing-context-active-change");
     64  }
     65 
     66  _createFrame() {
     67    this.frame = createDevToolsFrame(
     68      this.hostTab.ownerDocument,
     69      this.type == "bottom"
     70        ? "devtools-toolbox-bottom-iframe"
     71        : "devtools-toolbox-side-iframe"
     72    );
     73  }
     74 
     75  observe(subject, topic) {
     76    if (topic != "browsing-context-active-change") {
     77      return;
     78    }
     79    // Ignore any BrowsingContext which isn't the debugged tab's BrowsingContext
     80    // (toolbox may be half destroyed and the linkedBrowser be null when moving a tab
     81    // with DevTools to another window)
     82    if (this.hostTab.linkedBrowser?.browsingContext != subject) {
     83      return;
     84    }
     85 
     86    // In case this is called before create() is called
     87    if (!this.frame) {
     88      return;
     89    }
     90 
     91    // Update DevTools <browser> element's isActive according to the debugged <browser> element status.
     92    // This helps activate/deactivate DevTools when changing tabs.
     93    // It notably triggers visibilitychange events on DevTools documents.
     94    this.frame.docShellIsActive = subject.isActive;
     95  }
     96 
     97  /**
     98   * Raise the host.
     99   */
    100  raise() {
    101    focusTab(this.hostTab);
    102  }
    103 
    104  /**
    105   * Set the toolbox title.
    106   * Nothing to do for this host type.
    107   */
    108  setTitle() {}
    109 
    110  destroy() {
    111    Services.obs.removeObserver(this, "browsing-context-active-change");
    112    this._gBrowser = null;
    113    this._browserContainer = null;
    114  }
    115 }
    116 
    117 /**
    118 * Host object for the dock on the bottom of the browser
    119 */
    120 class BottomHost extends BaseInBrowserHost {
    121  constructor(hostTab) {
    122    super(hostTab, "bottom");
    123 
    124    this.heightPref = "devtools.toolbox.footer.height";
    125  }
    126 
    127  #splitter;
    128 
    129  #destroyed;
    130 
    131  /**
    132   * Create a box at the bottom of the host tab.
    133   */
    134  async create() {
    135    await gDevToolsBrowser.loadBrowserStyleSheet(this.hostTab.ownerGlobal);
    136 
    137    const { ownerDocument } = this.hostTab;
    138    this.#splitter = ownerDocument.createXULElement("splitter");
    139    this.#splitter.setAttribute("class", "devtools-horizontal-splitter");
    140    this.#splitter.setAttribute("resizebefore", "none");
    141    this.#splitter.setAttribute("resizeafter", "sibling");
    142 
    143    this._createFrame();
    144 
    145    this.frame.style.height =
    146      Math.min(
    147        Services.prefs.getIntPref(this.heightPref),
    148        this._browserContainer.clientHeight - MIN_PAGE_SIZE
    149      ) + "px";
    150 
    151    this._browserContainer.appendChild(this.#splitter);
    152    this._browserContainer.appendChild(this.frame);
    153    this.frame.docShellIsActive = true;
    154 
    155    focusTab(this.hostTab);
    156    return this.frame;
    157  }
    158 
    159  /**
    160   * Destroy the bottom dock.
    161   */
    162  destroy() {
    163    if (!this.#destroyed) {
    164      this.#destroyed = true;
    165 
    166      const height = parseInt(this.frame.style.height, 10);
    167      if (!isNaN(height)) {
    168        Services.prefs.setIntPref(this.heightPref, height);
    169      }
    170 
    171      this._browserContainer.removeChild(this.#splitter);
    172      this._browserContainer.removeChild(this.frame);
    173      this.frame = null;
    174      this.#splitter = null;
    175 
    176      super.destroy();
    177    }
    178 
    179    return Promise.resolve(null);
    180  }
    181 }
    182 
    183 /**
    184 * Base Host object for the in-browser left or right sidebars
    185 */
    186 class SidebarHost extends BaseInBrowserHost {
    187  constructor(hostTab, type) {
    188    super(hostTab, type);
    189 
    190    this.widthPref = "devtools.toolbox.sidebar.width";
    191  }
    192 
    193  #splitter;
    194  #browserPanel;
    195  #destroyed;
    196 
    197  /**
    198   * Create a box in the sidebar of the host tab.
    199   */
    200  async create() {
    201    await gDevToolsBrowser.loadBrowserStyleSheet(this.hostTab.ownerGlobal);
    202 
    203    this.#browserPanel = this._gBrowser.getPanel(this.hostTab.linkedBrowser);
    204    const { ownerDocument } = this.hostTab;
    205 
    206    this.#splitter = ownerDocument.createXULElement("splitter");
    207    this.#splitter.setAttribute("class", "devtools-side-splitter");
    208 
    209    this._createFrame();
    210 
    211    this.frame.style.width =
    212      Math.min(
    213        Services.prefs.getIntPref(this.widthPref),
    214        this.#browserPanel.clientWidth - MIN_PAGE_SIZE
    215      ) + "px";
    216 
    217    // We should consider the direction when changing the dock position.
    218    const topWindow = this.hostTab.ownerGlobal;
    219    const topDoc = topWindow.document.documentElement;
    220    const isLTR = topWindow.getComputedStyle(topDoc).direction === "ltr";
    221 
    222    this.#splitter.setAttribute("resizebefore", "none");
    223    this.#splitter.setAttribute("resizeafter", "none");
    224 
    225    if ((isLTR && this.type == "right") || (!isLTR && this.type == "left")) {
    226      this.#splitter.setAttribute("resizeafter", "sibling");
    227      this.#browserPanel.appendChild(this.#splitter);
    228      this.#browserPanel.appendChild(this.frame);
    229    } else {
    230      this.#splitter.setAttribute("resizebefore", "sibling");
    231      this.#browserPanel.insertBefore(this.frame, this._browserContainer);
    232      this.#browserPanel.insertBefore(this.#splitter, this._browserContainer);
    233    }
    234    this.frame.docShellIsActive = true;
    235 
    236    focusTab(this.hostTab);
    237    return this.frame;
    238  }
    239 
    240  /**
    241   * Destroy the sidebar.
    242   */
    243  destroy() {
    244    if (!this.#destroyed) {
    245      this.#destroyed = true;
    246 
    247      const width = parseInt(this.frame.style.width, 10);
    248      if (!isNaN(width)) {
    249        Services.prefs.setIntPref(this.widthPref, width);
    250      }
    251 
    252      this.#browserPanel.removeChild(this.#splitter);
    253      this.#browserPanel.removeChild(this.frame);
    254      this.#browserPanel = null;
    255      this.#splitter = null;
    256      this.frame = null;
    257 
    258      super.destroy();
    259    }
    260 
    261    return Promise.resolve(null);
    262  }
    263 }
    264 
    265 /**
    266 * Host object for the in-browser left sidebar
    267 */
    268 class LeftHost extends SidebarHost {
    269  constructor(hostTab) {
    270    super(hostTab, "left");
    271  }
    272 }
    273 
    274 /**
    275 * Host object for the in-browser right sidebar
    276 */
    277 class RightHost extends SidebarHost {
    278  constructor(hostTab) {
    279    super(hostTab, "right");
    280  }
    281 }
    282 
    283 /**
    284 * Host object for the toolbox in a separate window
    285 */
    286 class WindowHost extends EventEmitter {
    287  constructor(hostTab, options) {
    288    super();
    289 
    290    this._boundUnload = this._boundUnload.bind(this);
    291    this.hostTab = hostTab;
    292    this.options = options;
    293  }
    294 
    295  type = "window";
    296 
    297  WINDOW_URL = "chrome://devtools/content/framework/toolbox-window.xhtml";
    298 
    299  /**
    300   * Create a new xul window to contain the toolbox.
    301   */
    302  create() {
    303    return new Promise(resolve => {
    304      let flags = "chrome,centerscreen,resizable,dialog=no";
    305 
    306      // If we are debugging a tab which is in a Private window, we must also
    307      // set the private flag on the DevTools host window. Otherwise switching
    308      // hosts between docked and window modes can fail due to incompatible
    309      // docshell origin attributes. See 1581093.
    310      const owner = this.hostTab?.ownerGlobal;
    311      if (owner && lazy.PrivateBrowsingUtils.isWindowPrivate(owner)) {
    312        flags += ",private";
    313      }
    314 
    315      // If the current window is a non-fission window, force the non-fission
    316      // flag. Otherwise switching to window host from a non-fission window in
    317      // a fission Firefox (!) will attempt to swapFrameLoaders between fission
    318      // and non-fission frames. See Bug 1650963.
    319      if (this.hostTab && !this.hostTab.ownerGlobal.gFissionBrowser) {
    320        flags += ",non-fission";
    321      }
    322 
    323      // When debugging local Web Extension, the toolbox is opened in an
    324      // always foremost top level window in order to be kept visible
    325      // when interacting with the Firefox Window.
    326      if (this.options?.alwaysOnTop) {
    327        flags += ",alwaysontop";
    328      }
    329 
    330      const win = Services.ww.openWindow(
    331        null,
    332        this.WINDOW_URL,
    333        "_blank",
    334        flags,
    335        null
    336      );
    337 
    338      const frameLoad = () => {
    339        win.removeEventListener("load", frameLoad, true);
    340        win.focus();
    341 
    342        this.frame = createDevToolsFrame(
    343          win.document,
    344          "devtools-toolbox-window-iframe"
    345        );
    346        win.document
    347          .getElementById("devtools-toolbox-window")
    348          .appendChild(this.frame);
    349        this.frame.docShellIsActive = true;
    350 
    351        // The forceOwnRefreshDriver attribute is set to avoid Windows only issues with
    352        // CSS transitions when switching from docked to window hosts.
    353        // Added in Bug 832920, should be reviewed in Bug 1542468.
    354        this.frame.setAttribute("forceOwnRefreshDriver", "");
    355        resolve(this.frame);
    356      };
    357 
    358      win.addEventListener("load", frameLoad, true);
    359      win.addEventListener("unload", this._boundUnload);
    360 
    361      this._window = win;
    362    });
    363  }
    364 
    365  /**
    366   * Catch the user closing the window.
    367   */
    368  _boundUnload(event) {
    369    if (event.target.location != this.WINDOW_URL) {
    370      return;
    371    }
    372    this._window.removeEventListener("unload", this._boundUnload);
    373 
    374    this.emit("window-closed");
    375  }
    376 
    377  /**
    378   * Raise the host.
    379   */
    380  raise() {
    381    this._window.focus();
    382  }
    383 
    384  /**
    385   * Set the toolbox title.
    386   */
    387  setTitle(title) {
    388    this._window.document.title = title;
    389  }
    390 
    391  /**
    392   * Destroy the window.
    393   */
    394  destroy() {
    395    if (!this._destroyed) {
    396      this._destroyed = true;
    397 
    398      this._window.removeEventListener("unload", this._boundUnload);
    399      this._window.close();
    400    }
    401 
    402    return Promise.resolve(null);
    403  }
    404 }
    405 
    406 /**
    407 * Host object for the Browser Toolbox
    408 */
    409 class BrowserToolboxHost extends EventEmitter {
    410  constructor(hostTab, options) {
    411    super();
    412 
    413    this.doc = options.doc;
    414  }
    415 
    416  type = "browsertoolbox";
    417 
    418  async create() {
    419    this.frame = createDevToolsFrame(
    420      this.doc,
    421      "devtools-toolbox-browsertoolbox-iframe"
    422    );
    423 
    424    this.doc.body.appendChild(this.frame);
    425    this.frame.docShellIsActive = true;
    426 
    427    return this.frame;
    428  }
    429 
    430  /**
    431   * Raise the host.
    432   */
    433  raise() {
    434    this.doc.defaultView.focus();
    435  }
    436 
    437  /**
    438   * Set the toolbox title.
    439   */
    440  setTitle(title) {
    441    this.doc.title = title;
    442  }
    443 
    444  // Do nothing. The BrowserToolbox is destroyed by quitting the application.
    445  destroy() {
    446    return Promise.resolve(null);
    447  }
    448 }
    449 
    450 /**
    451 * Host object for the toolbox as a page.
    452 * This is typically used by `about:debugging`, when opening toolbox in a new tab,
    453 * via `about:devtools-toolbox` URLs.
    454 * The `iframe` ends up being the tab's browser element.
    455 */
    456 class PageHost {
    457  constructor(hostTab, options) {
    458    this.frame = options.customIframe;
    459  }
    460 
    461  type = "page";
    462 
    463  create() {
    464    return Promise.resolve(this.frame);
    465  }
    466 
    467  // Focus the tab owning the browser element.
    468  raise() {
    469    // See @constructor, for the page host, the frame is also the browser
    470    // element.
    471    focusTab(this.frame.ownerGlobal.gBrowser.getTabForBrowser(this.frame));
    472  }
    473 
    474  // Do nothing.
    475  setTitle() {}
    476 
    477  // Do nothing.
    478  destroy() {
    479    return Promise.resolve(null);
    480  }
    481 }
    482 
    483 /**
    484 *  Switch to the given tab in a browser and focus the browser window
    485 */
    486 function focusTab(tab) {
    487  const browserWindow = tab.ownerGlobal;
    488  browserWindow.focus();
    489  browserWindow.gBrowser.selectedTab = tab;
    490 }
    491 
    492 /**
    493 * Create an iframe that can be used to load DevTools via about:devtools-toolbox.
    494 */
    495 function createDevToolsFrame(doc, className) {
    496  const frame = doc.createXULElement("browser");
    497  frame.setAttribute("type", "content");
    498  frame.style.flex = "1 auto"; // Required to be able to shrink when the window shrinks
    499  frame.className = className;
    500 
    501  const inXULDocument = doc.documentElement.namespaceURI === XUL_NS;
    502  if (inXULDocument) {
    503    // When the toolbox frame is loaded in a XUL document, tooltips rely on a
    504    // special XUL <tooltip id="aHTMLTooltip"> element.
    505    // This attribute should not be set when the frame is loaded in a HTML
    506    // document (for instance: Browser Toolbox).
    507    frame.tooltip = "aHTMLTooltip";
    508  }
    509 
    510  // Allows toggling the `docShellIsActive` attribute
    511  frame.setAttribute("manualactiveness", "true");
    512  return frame;
    513 }
    514 
    515 exports.Hosts = {
    516  bottom: BottomHost,
    517  left: LeftHost,
    518  right: RightHost,
    519  window: WindowHost,
    520  browsertoolbox: BrowserToolboxHost,
    521  page: PageHost,
    522 };