tor-browser

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

IPProtectionPanel.sys.mjs (13424B)


      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 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  CustomizableUI:
      9    "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
     10  IPPEnrollAndEntitleManager:
     11    "moz-src:///browser/components/ipprotection/IPPEnrollAndEntitleManager.sys.mjs",
     12  IPPProxyManager:
     13    "moz-src:///browser/components/ipprotection/IPPProxyManager.sys.mjs",
     14  IPPProxyStates:
     15    "moz-src:///browser/components/ipprotection/IPPProxyManager.sys.mjs",
     16  IPProtectionService:
     17    "moz-src:///browser/components/ipprotection/IPProtectionService.sys.mjs",
     18  IPProtection:
     19    "moz-src:///browser/components/ipprotection/IPProtection.sys.mjs",
     20  IPPSignInWatcher:
     21    "moz-src:///browser/components/ipprotection/IPPSignInWatcher.sys.mjs",
     22 });
     23 
     24 import {
     25  LINKS,
     26  ERRORS,
     27 } from "chrome://browser/content/ipprotection/ipprotection-constants.mjs";
     28 
     29 let hasCustomElements = new WeakSet();
     30 
     31 /**
     32 * Manages updates for a IP Protection panelView in a given browser window.
     33 */
     34 export class IPProtectionPanel {
     35  static CONTENT_TAGNAME = "ipprotection-content";
     36  static CUSTOM_ELEMENTS_SCRIPT =
     37    "chrome://browser/content/ipprotection/ipprotection-customelements.js";
     38  static WIDGET_ID = "ipprotection-button";
     39  static PANEL_ID = "PanelUI-ipprotection";
     40  static TITLE_L10N_ID = "ipprotection-title";
     41  static HEADER_AREA_ID = "PanelUI-ipprotection-header";
     42  static CONTENT_AREA_ID = "PanelUI-ipprotection-content";
     43  static HEADER_BUTTON_ID = "ipprotection-header-button";
     44 
     45  /**
     46   * Loads the ipprotection custom element script
     47   * into a given window.
     48   *
     49   * Called on IPProtection.init for a new browser window.
     50   *
     51   * @param {Window} window
     52   */
     53  static loadCustomElements(window) {
     54    if (hasCustomElements.has(window)) {
     55      // Don't add the elements again for the same window.
     56      return;
     57    }
     58    Services.scriptloader.loadSubScriptWithOptions(
     59      IPProtectionPanel.CUSTOM_ELEMENTS_SCRIPT,
     60      {
     61        target: window,
     62        async: true,
     63      }
     64    );
     65    hasCustomElements.add(window);
     66  }
     67 
     68  /**
     69   * @typedef {object} State
     70   * @property {boolean} isProtectionEnabled
     71   *  The timestamp in milliseconds since IP Protection was enabled
     72   * @property {boolean} isSignedOut
     73   *  True if not signed in to account
     74   * @property {object} location
     75   *  Data about the server location the proxy is connected to
     76   * @property {string} location.name
     77   *  The location country name
     78   * @property {string} location.code
     79   *  The location country code
     80   * @property {"generic" | ""} error
     81   *  The error type as a string if an error occurred, or empty string if there are no errors.
     82   * @property {boolean} isAlpha
     83   *  True if we're running the Alpha variant, else false.
     84   * @property {boolean} hasUpgraded
     85   *  True if a Mozilla VPN subscription is linked to the user's Mozilla account.
     86   * @property {string} onboardingMessage
     87   * Continuous onboarding message to display in-panel, empty string if none applicable
     88   * @property {boolean} paused
     89   * True if the VPN service has been paused due to bandwidth limits
     90   */
     91 
     92  /**
     93   * @type {State}
     94   */
     95  state = {};
     96  panel = null;
     97  initiatedUpgrade = false;
     98 
     99  /**
    100   * Check the state of the enclosing panel to see if
    101   * it is active (open or showing).
    102   */
    103  get active() {
    104    let panelParent = this.panel?.closest("panel");
    105    if (!panelParent) {
    106      return false;
    107    }
    108    return panelParent.state == "open" || panelParent.state == "showing";
    109  }
    110 
    111  /**
    112   * Creates an instance of IPProtectionPanel for a specific browser window.
    113   *
    114   * Inserts the panel component customElements registry script.
    115   *
    116   * @param {Window} window
    117   *   Window containing the panelView to manage.
    118   */
    119  constructor(window) {
    120    this.handleEvent = this.#handleEvent.bind(this);
    121 
    122    this.state = {
    123      isSignedOut: !lazy.IPPSignInWatcher.isSignedIn,
    124      isProtectionEnabled:
    125        lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVE,
    126      location: {
    127        name: "United States",
    128        code: "us",
    129      },
    130      error: "",
    131      isAlpha: lazy.IPPEnrollAndEntitleManager.isAlpha,
    132      hasUpgraded: lazy.IPPEnrollAndEntitleManager.hasUpgraded,
    133      onboardingMessage: "",
    134      bandwidthWarning: "",
    135      paused: false,
    136    };
    137 
    138    if (window) {
    139      IPProtectionPanel.loadCustomElements(window);
    140    }
    141 
    142    this.#addProxyListeners();
    143  }
    144 
    145  /**
    146   * Set the state for this panel.
    147   *
    148   * Updates the current panel component state,
    149   * if the panel is currently active (showing or not hiding).
    150   *
    151   * @example
    152   * panel.setState({
    153   *  isSomething: true,
    154   * });
    155   *
    156   * @param {object} state
    157   *    The state object from IPProtectionPanel.
    158   */
    159  setState(state) {
    160    Object.assign(this.state, state);
    161 
    162    if (this.active) {
    163      this.updateState();
    164    }
    165  }
    166 
    167  /**
    168   * Updates the state of the panel component.
    169   *
    170   * @param {object} state
    171   *   The state object from IPProtectionPanel.
    172   * @param {Element} panelEl
    173   *   The panelEl element to update the state on.
    174   */
    175  updateState(state = this.state, panelEl = this.panel) {
    176    if (!panelEl?.isConnected || !panelEl.state) {
    177      return;
    178    }
    179 
    180    panelEl.state = state;
    181    panelEl.requestUpdate();
    182  }
    183 
    184  #startProxy() {
    185    lazy.IPPProxyManager.start();
    186  }
    187 
    188  #stopProxy() {
    189    lazy.IPPProxyManager.stop();
    190  }
    191 
    192  /**
    193   * Opens the help page in a new tab and closes the panel.
    194   *
    195   * @param {Event} e
    196   */
    197  static showHelpPage(e) {
    198    let win = e.target?.ownerGlobal;
    199    if (win) {
    200      win.openWebLinkIn(LINKS.SUPPORT_URL, "tab");
    201    }
    202 
    203    let panelParent = e.target?.closest("panel");
    204    if (panelParent) {
    205      panelParent.hidePopup();
    206    }
    207  }
    208 
    209  /**
    210   * Updates the visibility of the panel components before they will shown.
    211   *
    212   * - If the panel component has already been created, updates the state.
    213   * - Creates a panel component if need, state will be updated on once it has
    214   *   been connected.
    215   *
    216   * @param {XULElement} panelView
    217   *   The panelView element from the CustomizableUI widget callback.
    218   */
    219  showing(panelView) {
    220    if (this.initiatedUpgrade) {
    221      lazy.IPPEnrollAndEntitleManager.refetchEntitlement();
    222      this.initiatedUpgrade = false;
    223    }
    224 
    225    if (this.panel) {
    226      this.updateState();
    227    } else {
    228      this.#createPanel(panelView);
    229    }
    230 
    231    // TODO: Stop counting after all onboarding messages have been shown - Bug 1997332
    232    let currentCount = Services.prefs.getIntPref(
    233      "browser.ipProtection.panelOpenCount"
    234    );
    235    let updatedCount = currentCount + 1;
    236    Services.prefs.setIntPref(
    237      "browser.ipProtection.panelOpenCount",
    238      updatedCount
    239    );
    240  }
    241 
    242  /**
    243   * Called when the panel elements will be hidden.
    244   *
    245   * Disables updates to the panel.
    246   */
    247  hiding() {
    248    this.destroy();
    249  }
    250 
    251  /**
    252   * Creates a panel component in a panelView.
    253   *
    254   * @param {MozBrowser} panelView
    255   */
    256  #createPanel(panelView) {
    257    let { ownerDocument } = panelView;
    258 
    259    let headerArea = panelView.querySelector(
    260      `#${IPProtectionPanel.HEADER_AREA_ID}`
    261    );
    262    let headerButton = headerArea.querySelector(
    263      `#${IPProtectionPanel.HEADER_BUTTON_ID}`
    264    );
    265    if (!headerButton) {
    266      headerButton = this.#createHeaderButton(ownerDocument);
    267      headerArea.appendChild(headerButton);
    268    }
    269    // Reset the tab index to ensure it is focusable.
    270    headerButton.setAttribute("tabindex", "0");
    271 
    272    let contentEl = ownerDocument.createElement(
    273      IPProtectionPanel.CONTENT_TAGNAME
    274    );
    275    this.panel = contentEl;
    276 
    277    contentEl.dataset.capturesFocus = "true";
    278 
    279    this.#addPanelListeners(ownerDocument);
    280 
    281    let contentArea = panelView.querySelector(
    282      `#${IPProtectionPanel.CONTENT_AREA_ID}`
    283    );
    284    contentArea.appendChild(contentEl);
    285  }
    286 
    287  #createHeaderButton(ownerDocument) {
    288    const headerButton = ownerDocument.createXULElement("toolbarbutton");
    289 
    290    headerButton.id = IPProtectionPanel.HEADER_BUTTON_ID;
    291    headerButton.className = "panel-info-button";
    292    headerButton.dataset.capturesFocus = "true";
    293 
    294    ownerDocument.l10n.setAttributes(headerButton, "ipprotection-help-button");
    295    headerButton.addEventListener("click", IPProtectionPanel.showHelpPage);
    296    headerButton.addEventListener("keypress", e => {
    297      if (e.code == "Space" || e.code == "Enter") {
    298        IPProtectionPanel.showHelpPage(e);
    299      }
    300    });
    301    return headerButton;
    302  }
    303 
    304  /**
    305   * Open the IP Protection panel in the given window.
    306   *
    307   * @param {Window} window - which window to open the panel in.
    308   * @returns {Promise<void>}
    309   */
    310  async open(window) {
    311    if (!lazy.IPProtection.created || !window?.PanelUI) {
    312      return;
    313    }
    314 
    315    let widget = lazy.CustomizableUI.getWidget(IPProtectionPanel.WIDGET_ID);
    316    let anchor = widget.forWindow(window).anchor;
    317    await window.PanelUI.showSubView(IPProtectionPanel.PANEL_ID, anchor);
    318  }
    319 
    320  /**
    321   * Close the containing panel popup.
    322   */
    323  close() {
    324    let panelParent = this.panel?.closest("panel");
    325    if (!panelParent) {
    326      return;
    327    }
    328    panelParent.hidePopup();
    329  }
    330 
    331  /**
    332   * Start flow for signing in and then opening the panel on success
    333   */
    334  async startLoginFlow() {
    335    let window = this.panel.ownerGlobal;
    336    let browser = window.gBrowser;
    337    this.close();
    338    let isSignedIn = await lazy.IPProtectionService.startLoginFlow(browser);
    339    if (isSignedIn) {
    340      await this.open(window);
    341    }
    342  }
    343 
    344  /**
    345   * Remove added elements and listeners.
    346   */
    347  destroy() {
    348    if (this.panel) {
    349      this.panel.remove();
    350      this.#removePanelListeners(this.panel.ownerDocument);
    351      this.panel = null;
    352      if (this.state.error) {
    353        this.setState({
    354          error: "",
    355        });
    356      }
    357    }
    358  }
    359 
    360  uninit() {
    361    this.destroy();
    362    this.#removeProxyListeners();
    363  }
    364 
    365  #addPanelListeners(doc) {
    366    doc.addEventListener("IPProtection:Init", this.handleEvent);
    367    doc.addEventListener("IPProtection:ClickUpgrade", this.handleEvent);
    368    doc.addEventListener("IPProtection:Close", this.handleEvent);
    369    doc.addEventListener("IPProtection:UserEnable", this.handleEvent);
    370    doc.addEventListener("IPProtection:UserDisable", this.handleEvent);
    371    doc.addEventListener("IPProtection:SignIn", this.handleEvent);
    372    doc.addEventListener("IPProtection:UserShowSiteSettings", this.handleEvent);
    373  }
    374 
    375  #removePanelListeners(doc) {
    376    doc.removeEventListener("IPProtection:Init", this.handleEvent);
    377    doc.removeEventListener("IPProtection:ClickUpgrade", this.handleEvent);
    378    doc.removeEventListener("IPProtection:Close", this.handleEvent);
    379    doc.removeEventListener("IPProtection:UserEnable", this.handleEvent);
    380    doc.removeEventListener("IPProtection:UserDisable", this.handleEvent);
    381    doc.removeEventListener("IPProtection:SignIn", this.handleEvent);
    382    doc.removeEventListener(
    383      "IPProtection:UserShowSiteSettings",
    384      this.handleEvent
    385    );
    386  }
    387 
    388  #addProxyListeners() {
    389    lazy.IPProtectionService.addEventListener(
    390      "IPProtectionService:StateChanged",
    391      this.handleEvent
    392    );
    393    lazy.IPPProxyManager.addEventListener(
    394      "IPPProxyManager:StateChanged",
    395      this.handleEvent
    396    );
    397    lazy.IPPEnrollAndEntitleManager.addEventListener(
    398      "IPPEnrollAndEntitleManager:StateChanged",
    399      this.handleEvent
    400    );
    401  }
    402 
    403  #removeProxyListeners() {
    404    lazy.IPPEnrollAndEntitleManager.removeEventListener(
    405      "IPPEnrollAndEntitleManager:StateChanged",
    406      this.handleEvent
    407    );
    408    lazy.IPPProxyManager.removeEventListener(
    409      "IPPProxyManager:StateChanged",
    410      this.handleEvent
    411    );
    412    lazy.IPProtectionService.removeEventListener(
    413      "IPProtectionService:StateChanged",
    414      this.handleEvent
    415    );
    416  }
    417 
    418  #handleEvent(event) {
    419    if (event.type == "IPProtection:Init") {
    420      this.updateState();
    421    } else if (event.type == "IPProtection:Close") {
    422      this.close();
    423    } else if (event.type == "IPProtection:UserEnable") {
    424      this.#startProxy();
    425      Services.prefs.setBoolPref("browser.ipProtection.userEnabled", true);
    426    } else if (event.type == "IPProtection:UserDisable") {
    427      this.#stopProxy();
    428      Services.prefs.setBoolPref("browser.ipProtection.userEnabled", false);
    429    } else if (event.type == "IPProtection:ClickUpgrade") {
    430      // Let the service know that we tried upgrading at least once
    431      this.initiatedUpgrade = true;
    432      this.close();
    433    } else if (event.type == "IPProtection:SignIn") {
    434      this.startLoginFlow();
    435    } else if (
    436      event.type == "IPPProxyManager:StateChanged" ||
    437      event.type == "IPProtectionService:StateChanged" ||
    438      event.type === "IPPEnrollAndEntitleManager:StateChanged"
    439    ) {
    440      let hasError =
    441        lazy.IPPProxyManager.state === lazy.IPPProxyStates.ERROR &&
    442        lazy.IPPProxyManager.errors.includes(ERRORS.GENERIC);
    443 
    444      this.setState({
    445        isSignedOut: !lazy.IPPSignInWatcher.isSignedIn,
    446        isProtectionEnabled:
    447          lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVE,
    448        hasUpgraded: lazy.IPPEnrollAndEntitleManager.hasUpgraded,
    449        error: hasError ? ERRORS.GENERIC : "",
    450      });
    451    } else if (event.type == "IPProtection:UserShowSiteSettings") {
    452      // TODO: show subview for site settings (Bug 1997413)
    453    }
    454  }
    455 }