tor-browser

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

tab.js (10194B)


      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 "use strict";
      5 
      6 const {
      7  tabDescriptorSpec,
      8 } = require("resource://devtools/shared/specs/descriptors/tab.js");
      9 const DESCRIPTOR_TYPES = require("resource://devtools/client/fronts/descriptors/descriptor-types.js");
     10 
     11 loader.lazyRequireGetter(
     12  this,
     13  "gDevTools",
     14  "resource://devtools/client/framework/devtools.js",
     15  true
     16 );
     17 loader.lazyRequireGetter(
     18  this,
     19  "WindowGlobalTargetFront",
     20  "resource://devtools/client/fronts/targets/window-global.js",
     21  true
     22 );
     23 const {
     24  FrontClassWithSpec,
     25  registerFront,
     26 } = require("resource://devtools/shared/protocol.js");
     27 const {
     28  DescriptorMixin,
     29 } = require("resource://devtools/client/fronts/descriptors/descriptor-mixin.js");
     30 
     31 const POPUP_DEBUG_PREF = "devtools.popups.debug";
     32 
     33 /**
     34 * DescriptorFront for tab targets.
     35 *
     36 * @fires remoteness-change
     37 *        Fired only for target switching, when the debugged tab is a local tab.
     38 *        TODO: This event could move to the server in order to support
     39 *        remoteness change for remote debugging.
     40 */
     41 class TabDescriptorFront extends DescriptorMixin(
     42  FrontClassWithSpec(tabDescriptorSpec)
     43 ) {
     44  constructor(client, targetFront, parentFront) {
     45    super(client, targetFront, parentFront);
     46 
     47    // The tab descriptor can be configured to create either local tab targets
     48    // (eg, regular tab toolbox) or browsing context targets (eg tab remote
     49    // debugging).
     50    this._localTab = null;
     51 
     52    // Flag to prevent the server from trying to spawn targets by the watcher actor.
     53    this._disableTargetSwitching = false;
     54 
     55    this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
     56    this._handleTabEvent = this._handleTabEvent.bind(this);
     57 
     58    // When the target is created from the server side,
     59    // it is not created via TabDescriptor.getTarget.
     60    // Instead, it is retrieved by the TargetCommand which
     61    // will call TabDescriptor.setTarget from TargetCommand.onTargetAvailable
     62    if (this.isServerTargetSwitchingEnabled()) {
     63      this._targetFrontPromise = new Promise(
     64        r => (this._resolveTargetFrontPromise = r)
     65      );
     66    }
     67  }
     68 
     69  descriptorType = DESCRIPTOR_TYPES.TAB;
     70 
     71  form(json) {
     72    this.actorID = json.actor;
     73    this._form = json;
     74    this.traits = json.traits || {};
     75  }
     76 
     77  /**
     78   * Destroy the front.
     79   *
     80   * @param Boolean If true, it means that we destroy the front when receiving the descriptor-destroyed
     81   *                event from the server.
     82   */
     83  destroy({ isServerDestroyEvent = false } = {}) {
     84    if (this.isDestroyed()) {
     85      return;
     86    }
     87 
     88    // The descriptor may be destroyed first by the frontend.
     89    // When closing the tab, the toolbox document is almost immediately removed from the DOM.
     90    // The `unload` event fires and toolbox destroys itself, as well as its related client.
     91    //
     92    // In such case, we emit the descriptor-destroyed event
     93    if (!isServerDestroyEvent) {
     94      this.emit("descriptor-destroyed");
     95    }
     96    if (this.isLocalTab) {
     97      this._teardownLocalTabListeners();
     98    }
     99    super.destroy();
    100  }
    101 
    102  getWatcher() {
    103    const isPopupDebuggingEnabled = Services.prefs.getBoolPref(
    104      POPUP_DEBUG_PREF,
    105      false
    106    );
    107    return super.getWatcher({
    108      isServerTargetSwitchingEnabled: this.isServerTargetSwitchingEnabled(),
    109      isPopupDebuggingEnabled,
    110    });
    111  }
    112 
    113  setLocalTab(localTab) {
    114    this._localTab = localTab;
    115    this._setupLocalTabListeners();
    116  }
    117 
    118  get isTabDescriptor() {
    119    return true;
    120  }
    121 
    122  get isLocalTab() {
    123    return !!this._localTab;
    124  }
    125 
    126  get localTab() {
    127    return this._localTab;
    128  }
    129 
    130  _setupLocalTabListeners() {
    131    this.localTab.addEventListener("TabClose", this._handleTabEvent);
    132    this.localTab.addEventListener("TabRemotenessChange", this._handleTabEvent);
    133  }
    134 
    135  _teardownLocalTabListeners() {
    136    this.localTab.removeEventListener("TabClose", this._handleTabEvent);
    137    this.localTab.removeEventListener(
    138      "TabRemotenessChange",
    139      this._handleTabEvent
    140    );
    141  }
    142 
    143  isServerTargetSwitchingEnabled() {
    144    return !this._disableTargetSwitching;
    145  }
    146 
    147  /**
    148   * Called by CommandsFactory, when the WebExtension codebase instantiates
    149   * a commands. We have to flag the TabDescriptor for them as they don't support
    150   * target switching and gets severely broken when enabling server target which
    151   * introduce target switching for all navigations and reloads
    152   */
    153  setIsForWebExtension() {
    154    this.disableTargetSwitching();
    155  }
    156 
    157  /**
    158   * Method used by the WebExtension which still need to disable server side targets,
    159   * and also a few xpcshell tests which are using legacy API and don't support watcher actor.
    160   */
    161  disableTargetSwitching() {
    162    this._disableTargetSwitching = true;
    163    // Delete these two attributes which have to be set early from the constructor,
    164    // but we don't know yet if target switch should be disabled.
    165    delete this._targetFrontPromise;
    166    delete this._resolveTargetFrontPromise;
    167  }
    168 
    169  get isZombieTab() {
    170    return this._form.isZombieTab;
    171  }
    172 
    173  get browserId() {
    174    return this._form.browserId;
    175  }
    176 
    177  get selected() {
    178    return this._form.selected;
    179  }
    180 
    181  get title() {
    182    return this._form.title;
    183  }
    184 
    185  get url() {
    186    return this._form.url;
    187  }
    188 
    189  get favicon() {
    190    // Note: the favicon is not part of the default form() payload, it will be
    191    // added in `retrieveFavicon`.
    192    return this._form.favicon;
    193  }
    194 
    195  _createTabTarget(form) {
    196    const front = new WindowGlobalTargetFront(this._client, null, this);
    197 
    198    // As these fronts aren't instantiated by protocol.js, we have to set their actor ID
    199    // manually like that:
    200    front.actorID = form.actor;
    201    front.form(form);
    202    this.manage(front);
    203    return front;
    204  }
    205 
    206  _onTargetDestroyed() {
    207    // Clear the cached targetFront when the target is destroyed.
    208    // Note that we are also checking that _targetFront has a valid actorID
    209    // in getTarget, this acts as an additional security to avoid races.
    210    this._targetFront = null;
    211  }
    212 
    213  /**
    214   * Safely retrieves the favicon via getFavicon() and populates this._form.favicon.
    215   *
    216   * We could let callers explicitly retrieve the favicon instead of inserting it in the
    217   * form dynamically.
    218   */
    219  async retrieveFavicon() {
    220    try {
    221      this._form.favicon = await this.getFavicon();
    222    } catch (e) {
    223      // We might request the data for a tab which is going to be destroyed.
    224      // In this case the TargetFront will be destroyed. Otherwise log an error.
    225      if (!this.isDestroyed()) {
    226        console.error("Failed to retrieve the favicon for " + this.url, e);
    227      }
    228    }
    229  }
    230 
    231  /**
    232   * Top-level targets created on the server will not be created and managed
    233   * by a descriptor front. Instead they are created by the Watcher actor.
    234   * On the client side we manually re-establish a link between the descriptor
    235   * and the new top-level target.
    236   */
    237  setTarget(targetFront) {
    238    // Completely ignore the previous target.
    239    // We might nullify the _targetFront unexpectely due to previous target
    240    // being destroyed after the new is created
    241    if (this._targetFront) {
    242      this._targetFront.off("target-destroyed", this._onTargetDestroyed);
    243    }
    244    this._targetFront = targetFront;
    245 
    246    targetFront.on("target-destroyed", this._onTargetDestroyed);
    247 
    248    if (this.isServerTargetSwitchingEnabled()) {
    249      this._resolveTargetFrontPromise(targetFront);
    250 
    251      // Set a new promise in order to:
    252      // 1) Avoid leaking the targetFront we just resolved into the previous promise.
    253      // 2) Never return an empty target from `getTarget`
    254      //
    255      // About the second point:
    256      // There is a race condition where we call `onTargetDestroyed` (which clears `this.targetFront`)
    257      // a bit before calling `setTarget`. So that `this.targetFront` could be null,
    258      // while we now a new target will eventually come when calling `setTarget`.
    259      // Setting a new promise will help wait for the next target while `_targetFront` is null.
    260      // Note that `getTarget` first look into `_targetFront` before checking for `_targetFrontPromise`.
    261      this._targetFrontPromise = new Promise(
    262        r => (this._resolveTargetFrontPromise = r)
    263      );
    264    }
    265  }
    266  getCachedTarget() {
    267    return this._targetFront;
    268  }
    269  async getTarget() {
    270    if (this._targetFront && !this._targetFront.isDestroyed()) {
    271      return this._targetFront;
    272    }
    273 
    274    if (this._targetFrontPromise) {
    275      return this._targetFrontPromise;
    276    }
    277 
    278    this._targetFrontPromise = (async () => {
    279      let newTargetFront = null;
    280      try {
    281        const targetForm = await super.getTarget();
    282        newTargetFront = this._createTabTarget(targetForm);
    283        this.setTarget(newTargetFront);
    284      } catch (e) {
    285        console.log(
    286          `Request to connect to TabDescriptor "${this.id}" failed: ${e}`
    287        );
    288      }
    289 
    290      this._targetFrontPromise = null;
    291      return newTargetFront;
    292    })();
    293    return this._targetFrontPromise;
    294  }
    295 
    296  /**
    297   * Handle tabs events.
    298   */
    299  async _handleTabEvent(event) {
    300    switch (event.type) {
    301      case "TabClose": {
    302        // Always destroy the toolbox opened for this local tab descriptor.
    303        // When the toolbox is in a Window Host, it won't be removed from the
    304        // DOM when the tab is closed.
    305        const toolbox = gDevTools.getToolboxForDescriptorFront(this);
    306        if (toolbox) {
    307          // Toolbox.destroy will call target.destroy eventually.
    308          await toolbox.destroy();
    309        }
    310        break;
    311      }
    312      case "TabRemotenessChange":
    313        this._onRemotenessChange();
    314        break;
    315    }
    316  }
    317 
    318  /**
    319   * Automatically respawn the toolbox when the tab changes between being
    320   * loaded within the parent process and loaded from a content process.
    321   * Process change can go in both ways.
    322   */
    323  async _onRemotenessChange() {
    324    // In a near future, this client side code should be replaced by actor code,
    325    // notifying about new tab targets.
    326    this.emit("remoteness-change", this._targetFront);
    327  }
    328 }
    329 
    330 registerFront(TabDescriptorFront);