- : 80 %
- : 80 %
- : 80 %
- : 80 %
- : 80 %
- : 96 %
- : 96 %
- : 98 %
- : 98 %
- : 98 %
- : 86 %
- : 98 %
- : 98 %
- : 98 %
- : 86 %
- : 86 %
- : 86 %
- : 86 %
- : 98 %
- : 98 %
- : 86 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 96 %
- : 85 %
- : 96 %
- : 96 %
- : 96 %
- : 85 %
- : 87 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 94 %
- : 94 %
- : 94 %
- : 94 %
- : 94 %
- : 94 %
- : 94 %
- : 94 %
- : 94 %
- : 94 %
- : 94 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 93 %
- : 97 %
- : 97 %
- : 97 %
- : 96 %
- : 92 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 92 %
- : 96 %
- : 94 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 97 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 95 %
- : 95 %
- : 96 %
- : 96 %
- : 96 %
- : 96 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 94 %
- : 95 %
- : 95 %
- : 95 %
- : 94 %
- : 94 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 94 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 95 %
- : 96 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 97 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 40 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
- : 98 %
Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
CustomizableUI:
"moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
IPPEnrollAndEntitleManager:
"moz-src:///toolkit/components/ipprotection/IPPEnrollAndEntitleManager.sys.mjs",
IPPExceptionsManager:
"moz-src:///toolkit/components/ipprotection/IPPExceptionsManager.sys.mjs",
IPPOnboardingMessage:
"moz-src:///browser/components/ipprotection/IPPOnboardingMessageHelper.sys.mjs",
ERRORS: "moz-src:///toolkit/components/ipprotection/IPPProxyManager.sys.mjs",
IPPProxyManager:
"moz-src:///toolkit/components/ipprotection/IPPProxyManager.sys.mjs",
IPPProxyStates:
"moz-src:///toolkit/components/ipprotection/IPPProxyManager.sys.mjs",
IPPUsageHelper:
"moz-src:///browser/components/ipprotection/IPPUsageHelper.sys.mjs",
UsageStates:
"moz-src:///browser/components/ipprotection/IPPUsageHelper.sys.mjs",
IPProtectionService:
"moz-src:///toolkit/components/ipprotection/IPProtectionService.sys.mjs",
IPProtection:
"moz-src:///browser/components/ipprotection/IPProtection.sys.mjs",
IPPSignInWatcher:
"moz-src:///toolkit/components/ipprotection/IPPSignInWatcher.sys.mjs",
IPProtectionStates:
"moz-src:///toolkit/components/ipprotection/IPProtectionService.sys.mjs",
SpecialMessageActions:
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
});
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import {
BANDWIDTH,
ONBOARDING_PREF_FLAGS,
LINKS,
SIGNIN_DATA,
} from "chrome://browser/content/ipprotection/ipprotection-constants.mjs";
const BANDWIDTH_THRESHOLD_PREF = "browser.ipProtection.bandwidthThreshold";
const DEFAULT_EGRESS_LOCATION = { name: "United States", code: "us" };
const EGRESS_LOCATION_PREF = "browser.ipProtection.egressLocationEnabled";
const USER_OPENED_PREF = "browser.ipProtection.everOpenedPanel";
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"BANDWIDTH_USAGE_ENABLED",
"browser.ipProtection.bandwidth.enabled",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"EGRESS_LOCATION_ENABLED",
EGRESS_LOCATION_PREF,
false
);
let hasCustomElements = new WeakSet();
/**
* Manages updates for a IP Protection panelView in a given browser window.
*/
export class IPProtectionPanel {
static CONTENT_TAGNAME = "ipprotection-content";
static CUSTOM_ELEMENTS_SCRIPT =
"chrome://browser/content/ipprotection/ipprotection-customelements.js";
static WIDGET_ID = "ipprotection-button";
static PANEL_ID = "PanelUI-ipprotection";
static TITLE_L10N_ID = "ipprotection-title";
static HEADER_AREA_ID = "PanelUI-ipprotection-header";
static CONTENT_AREA_ID = "PanelUI-ipprotection-content";
static HEADER_BUTTON_ID = "ipprotection-header-button";
/**
* Loads the ipprotection custom element script
* into a given window.
*
* Called on IPProtection.init for a new browser window.
*
* @param {Window} window
*/
static loadCustomElements(window) {
if (hasCustomElements.has(window)) {
// Don't add the elements again for the same window.
return;
}
Services.scriptloader.loadSubScriptWithOptions(
IPProtectionPanel.CUSTOM_ELEMENTS_SCRIPT,
{
target: window,
async: true,
}
);
hasCustomElements.add(window);
}
/**
* @typedef {object} State
* @property {boolean} isProtectionEnabled
* The timestamp in milliseconds since IP Protection was enabled
* @property {boolean} isSignedOut
* True if not signed in to account
* @property {object} location
* Data about the server location the proxy is connected to
* @property {string} location.name
* The location country name
* @property {string} location.code
* The location country code
* @property {"generic-error" | "network-error" | ""} error
* The error type as a string if an error occurred, or empty string if there are no errors.
* @property {boolean} hasUpgraded
* True if a Mozilla VPN subscription is linked to the user's Mozilla account.
* @property {string} onboardingMessage
* Continuous onboarding message to display in-panel, empty string if none applicable
* @property {boolean} paused
* True if the VPN service has been paused due to bandwidth limits
* @property {boolean} isSiteExceptionsEnabled
* True if site exceptions support is enabled, else false.
* @property {object} siteData
* Data about the currently loaded site, including "isExclusion".
* @property {object} bandwidthUsage
* An object containing the current and max usage
* @property {boolean} isActivating
* True if the VPN service is in the process of connecting, else false.
*/
/**
* @type {State}
*/
state = {};
panel = null;
initiatedUpgrade = false;
#window = null;
#lastDismissedUsageState = "none";
#panelView = null;
// Bug 2020733: Adds a key listener at the panel level
// since moz-button (header button) traps key events in its shadow DOM.
// This also avoids duplicate listeners across panel components.
#panelKeyListener = e => {
let { code } = e;
if (code !== "ArrowDown" && code !== "ArrowUp") {
return;
}
e.stopPropagation();
e.preventDefault();
let direction =
code === "ArrowDown"
? Services.focus.MOVEFOCUS_FORWARD
: Services.focus.MOVEFOCUS_BACKWARD;
Services.focus.moveFocus(
e.target.ownerGlobal,
null,
direction,
Services.focus.FLAG_BYKEY
);
};
/**
* Gets the gBrowser from the weak reference to the window.
*
* @returns {object|undefined}
* The gBrowser object, or undefined if the window has been garbage collected.
*/
get gBrowser() {
const win = this.#window.get();
return win?.gBrowser;
}
/**
* Gets the toolbar for this panel's window
*
* @return {IPProtectionToolbarButton|undefined}
* The toolbarbutton element, or undefined if the window has been garbage collected.
*/
get toolbarButton() {
const win = this.#window.get();
return lazy.IPProtection.getToolbarButton(win);
}
/**
* Check the state of the enclosing panel to see if
* it is active (open or showing).
*/
get active() {
let panelParent = this.panel?.closest("panel");
if (!panelParent) {
return false;
}
return panelParent.state == "open" || panelParent.state == "showing";
}
/**
* Gets the value of the pref
* browser.ipProtection.features.siteExceptions.
*/
get isExceptionsFeatureEnabled() {
return Services.prefs.getBoolPref(
"browser.ipProtection.features.siteExceptions",
false
);
}
/**
* Creates an instance of IPProtectionPanel for a specific browser window.
*
* Inserts the panel component customElements registry script.
*
* @param {Window} window
* Window containing the panelView to manage.
*/
constructor(window) {
this.#window = Cu.getWeakReference(window);
this.handleEvent = this.#handleEvent.bind(this);
this.handlePrefChange = this.#handlePrefChange.bind(this);
this.state = {
isSignedOut: !lazy.IPPSignInWatcher.isSignedIn,
unauthenticated:
lazy.IPProtectionService.state ===
lazy.IPProtectionStates.UNAUTHENTICATED,
isProtectionEnabled:
lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVE,
location: lazy.EGRESS_LOCATION_ENABLED ? DEFAULT_EGRESS_LOCATION : null,
error: "",
hasUpgraded: lazy.IPPEnrollAndEntitleManager.hasUpgraded,
onboardingMessage: "",
bandwidthWarning: false,
paused: lazy.IPPProxyManager.state === lazy.IPPProxyStates.PAUSED,
isSiteExceptionsEnabled: this.isExceptionsFeatureEnabled,
siteData: this.#getSiteData(),
bandwidthUsage: this.#getBandwidthUsage(),
isActivating:
lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVATING,
};
// The progress listener to listen for page navigations.
// Used to update the siteData state property for site exclusions.
this.progressListener = {
onLocationChange: (
aBrowser,
aWebProgress,
_aRequest,
aLocationURI,
_aFlags
) => {
if (!aWebProgress.isTopLevel) {
return;
}
// Only update if on the currently selected tab
if (aBrowser !== this.gBrowser?.selectedBrowser) {
return;
}
if (this.active && aLocationURI) {
this.#updateSiteData();
}
},
};
const win = this.#window.get();
if (win) {
IPProtectionPanel.loadCustomElements(win);
}
this.#addProxyListeners();
this.#addProgressListener();
this.#addPrefObserver();
}
/**
* Set the state for this panel.
*
* Updates the current panel component state,
* if the panel is currently active (showing or not hiding).
*
* @example
* panel.setState({
* isSomething: true,
* });
*
* @param {object} state
* The state object from IPProtectionPanel.
*/
setState(state) {
Object.assign(this.state, state);
if (this.active) {
this.updateState();
}
}
/**
* Updates the state of the panel component.
*
* @param {object} state
* The state object from IPProtectionPanel.
* @param {Element} panelEl
* The panelEl element to update the state on.
*/
updateState(state = this.state, panelEl = this.panel) {
if (!panelEl?.isConnected || !panelEl.state) {
return;
}
panelEl.state = state;
panelEl.requestUpdate();
}
async #startProxy() {
const win = this.#window.get();
const inPrivateBrowsing =
!!win && lazy.PrivateBrowsingUtils.isWindowPrivate(win);
const { error } = await lazy.IPPProxyManager.start(true, inPrivateBrowsing);
if (error && error !== lazy.ERRORS.CANCELED) {
const errorMessage =
error == lazy.ERRORS.NETWORK
? lazy.ERRORS.NETWORK
: lazy.ERRORS.GENERIC;
this.setState({
error: errorMessage,
});
this.toolbarButton?.updateState(null, { error: errorMessage });
}
}
async #stopProxy() {
await lazy.IPPProxyManager.stop();
}
/**
* Opens the help page in a new tab and closes the panel.
*
* @param {Event} e
*/
static showHelpPage(e) {
let win = e.target?.ownerGlobal;
if (win) {
win.openWebLinkIn(
Services.urlFormatter.formatURLPref("app.support.baseURL") +
LINKS.SUPPORT_SLUG,
"tab"
);
}
let panelParent = e.target?.closest("panel");
if (panelParent) {
panelParent.hidePopup();
}
}
/**
* Updates the visibility of the panel components before they will shown.
*
* - If the panel component has already been created, updates the state.
* - Creates a panel component if need, state will be updated on once it has
* been connected.
*
* @param {XULElement} panelView
* The panelView element from the CustomizableUI widget callback.
*/
showing(panelView) {
if (this.initiatedUpgrade) {
lazy.IPPEnrollAndEntitleManager.refetchEntitlement();
this.initiatedUpgrade = false;
}
this.#updateSiteData();
this.setState({
isSiteExceptionsEnabled: this.isExceptionsFeatureEnabled,
bandwidthWarning: this.#shouldShowBandwidthWarning(),
});
if (this.panel) {
this.updateState();
} else {
this.#createPanel(panelView);
}
let hasUserEverOpenedPanel = Services.prefs.getBoolPref(USER_OPENED_PREF);
if (!hasUserEverOpenedPanel) {
Services.prefs.setBoolPref(USER_OPENED_PREF, true);
}
}
/**
* Called when the panel elements will be hidden.
*
* Disables updates to the panel.
*/
hiding() {
const mask = lazy.IPPOnboardingMessage.readPrefMask();
const hasUsedSiteExceptions = !!(
mask & ONBOARDING_PREF_FLAGS.EVER_USED_SITE_EXCEPTIONS
);
const browser = this.gBrowser.selectedBrowser;
lazy.ASRouter.sendTriggerMessage({
browser,
id: "ipProtectionPanelClosed",
context: {
hasUsedSiteExceptions,
},
});
this.destroy();
}
/**
* Creates a panel component in a panelView.
*
* @param {MozBrowser} panelView
*/
#createPanel(panelView) {
let { ownerDocument } = panelView;
let headerArea = panelView.querySelector(
`#${IPProtectionPanel.HEADER_AREA_ID}`
);
let headerButton = headerArea.querySelector(
`#${IPProtectionPanel.HEADER_BUTTON_ID}`
);
if (!headerButton) {
headerButton = this.#createHeaderButton(ownerDocument);
headerArea.appendChild(headerButton);
}
// Reset the tab index to ensure it is focusable.
headerButton.setAttribute("tabindex", "0");
let contentEl = ownerDocument.createElement(
IPProtectionPanel.CONTENT_TAGNAME
);
this.panel = contentEl;
contentEl.dataset.capturesFocus = "true";
this.#panelView = panelView;
panelView.addEventListener("keydown", this.#panelKeyListener, {
capture: true,
});
this.#addPanelListeners(ownerDocument);
let contentArea = panelView.querySelector(
`#${IPProtectionPanel.CONTENT_AREA_ID}`
);
contentArea.appendChild(contentEl);
}
#createHeaderButton(ownerDocument) {
const headerButton = ownerDocument.createElement("moz-button");
headerButton.id = IPProtectionPanel.HEADER_BUTTON_ID;
headerButton.className = "panel-info-button";
headerButton.dataset.capturesFocus = "true";
headerButton.type = "ghost";
headerButton.iconSrc = "chrome://global/skin/icons/info.svg";
headerButton.size = "small";
ownerDocument.l10n.setAttributes(headerButton, "ipprotection-help-button");
headerButton.addEventListener("click", IPProtectionPanel.showHelpPage);
headerButton.addEventListener("keypress", e => {
if (e.code == "Space" || e.code == "Enter") {
IPProtectionPanel.showHelpPage(e);
}
});
return headerButton;
}
/**
* Open the IP Protection panel in the given window.
*
* @param {Window} window - which window to open the panel in.
* @returns {Promise<void>}
*/
async open(window = this.#window.get()) {
if (!lazy.IPProtection.created || !window?.PanelUI || this.active) {
return;
}
let widget = lazy.CustomizableUI.getWidget(IPProtectionPanel.WIDGET_ID);
let anchor = widget.forWindow(window).anchor;
await window.PanelUI.showSubView(IPProtectionPanel.PANEL_ID, anchor);
}
/**
* Close the containing panel popup.
*/
close() {
let panelParent = this.panel?.closest("panel");
if (!panelParent) {
return;
}
panelParent.hidePopup();
}
/**
* Start flow for signing in and then opening the panel on success
*
* @param {object} options
* @param {string} options.entrypoint
* The entrypoint to pass for the sign in flow
* @param {string} options.utm_source
* The utm_source to pass for the sign in flow
*/
async startLoginFlow({
entrypoint = "vpn_integration_panel",
utm_source = "panel",
} = {}) {
let window = this.#window.get();
let browser = window.gBrowser;
// Close the panel if the user will need to sign in.
this.close();
const signedIn = await lazy.SpecialMessageActions.fxaSignInFlow(
{
...SIGNIN_DATA,
entrypoint,
extraParams: { ...SIGNIN_DATA.extraParams, utm_source },
},
browser
);
return signedIn;
}
/**
* Ensure there is a signed in account and then open the panel after enrolling.
*
* @param {object} options
*/
async enroll(options = {}) {
Glean.ipprotection.getStarted.record();
const signedIn = await this.startLoginFlow(options);
if (!signedIn) {
return;
}
// Temporarily set the main panel view to show if enrolling.
this.setState({
unauthenticated: false,
});
// Asynchronously enroll and entitle the user.
// It will only need to finish before the proxy can start.
const enrolling = lazy.IPPEnrollAndEntitleManager.maybeEnrollAndEntitle();
if (!this.active) {
await this.open();
}
const result = await enrolling;
Glean.ipprotection.enrollment.record({
enrolled: result?.isEnrolledAndEntitled,
});
}
/**
* Remove added elements and listeners.
*/
destroy() {
if (this.panel) {
const doc = this.panel.ownerDocument;
this.#panelView?.removeEventListener("keydown", this.#panelKeyListener, {
capture: true,
});
this.#panelView = null;
this.panel.remove();
this.#removePanelListeners(doc);
this.panel = null;
if (this.state.error) {
this.setState({
error: "",
});
this.toolbarButton?.updateState(null, { error: "" });
}
}
}
uninit() {
this.destroy();
this.#removeProxyListeners();
this.#removeProgressListener();
this.#removePrefObserver();
}
#addPanelListeners(doc) {
doc.addEventListener("IPProtection:Init", this.handleEvent);
doc.addEventListener("IPProtection:ClickUpgrade", this.handleEvent);
doc.addEventListener("IPProtection:Close", this.handleEvent);
doc.addEventListener("IPProtection:UserEnable", this.handleEvent);
doc.addEventListener("IPProtection:UserDisable", this.handleEvent);
doc.addEventListener("IPProtection:OptIn", this.handleEvent);
doc.addEventListener("IPProtection:UserEnableVPNForSite", this.handleEvent);
doc.addEventListener(
"IPProtection:UserDisableVPNForSite",
this.handleEvent
);
doc.addEventListener(
"IPProtection:DismissBandwidthWarning",
this.handleEvent
);
}
#removePanelListeners(doc) {
doc.removeEventListener("IPProtection:Init", this.handleEvent);
doc.removeEventListener("IPProtection:ClickUpgrade", this.handleEvent);
doc.removeEventListener("IPProtection:Close", this.handleEvent);
doc.removeEventListener("IPProtection:UserEnable", this.handleEvent);
doc.removeEventListener("IPProtection:UserDisable", this.handleEvent);
doc.removeEventListener("IPProtection:OptIn", this.handleEvent);
doc.removeEventListener(
"IPProtection:UserEnableVPNForSite",
this.handleEvent
);
doc.removeEventListener(
"IPProtection:UserDisableVPNForSite",
this.handleEvent
);
doc.removeEventListener(
"IPProtection:DismissBandwidthWarning",
this.handleEvent
);
}
#addProxyListeners() {
lazy.IPProtectionService.addEventListener(
"IPProtectionService:StateChanged",
this.handleEvent
);
lazy.IPPProxyManager.addEventListener(
"IPPProxyManager:StateChanged",
this.handleEvent
);
lazy.IPPProxyManager.addEventListener(
"IPPProxyManager:UsageChanged",
this.handleEvent
);
lazy.IPPUsageHelper.addEventListener(
"IPPUsageHelper:StateChanged",
this.handleEvent
);
lazy.IPPEnrollAndEntitleManager.addEventListener(
"IPPEnrollAndEntitleManager:StateChanged",
this.handleEvent
);
lazy.IPPExceptionsManager.addEventListener(
"IPPExceptionsManager:ExclusionChanged",
this.handleEvent
);
}
#removeProxyListeners() {
lazy.IPPEnrollAndEntitleManager.removeEventListener(
"IPPEnrollAndEntitleManager:StateChanged",
this.handleEvent
);
lazy.IPPProxyManager.removeEventListener(
"IPPProxyManager:StateChanged",
this.handleEvent
);
lazy.IPPProxyManager.removeEventListener(
"IPPProxyManager:UsageChanged",
this.handleEvent
);
lazy.IPPUsageHelper.removeEventListener(
"IPPUsageHelper:StateChanged",
this.handleEvent
);
lazy.IPProtectionService.removeEventListener(
"IPProtectionService:StateChanged",
this.handleEvent
);
lazy.IPPExceptionsManager.removeEventListener(
"IPPExceptionsManager:ExclusionChanged",
this.handleEvent
);
}
#shouldShowBandwidthWarning() {
const state = lazy.IPPUsageHelper.state;
if (
(state == "warning-75-percent" || state == "warning-90-percent") &&
state !== this.#lastDismissedUsageState
) {
return true;
}
return false;
}
#addProgressListener() {
if (this.gBrowser) {
this.gBrowser.addTabsProgressListener(this.progressListener);
}
}
#removeProgressListener() {
if (this.gBrowser) {
this.gBrowser.removeTabsProgressListener(this.progressListener);
}
}
#addPrefObserver() {
Services.prefs.addObserver(EGRESS_LOCATION_PREF, this.handlePrefChange);
}
#removePrefObserver() {
Services.prefs.removeObserver(EGRESS_LOCATION_PREF, this.handlePrefChange);
}
#handlePrefChange(subject, topic, data) {
if (data === EGRESS_LOCATION_PREF) {
const isEnabled = Services.prefs.getBoolPref(EGRESS_LOCATION_PREF, false);
this.setState({
location: isEnabled ? DEFAULT_EGRESS_LOCATION : null,
});
}
}
/**
* Gets siteData by reading the current content principal.
*
* @returns {object|null}
* An object with data relevant to a site (eg. isExclusion),
* or null otherwise if invalid.
*
* @see State.siteData
*/
#getSiteData() {
const principal = this.gBrowser?.contentPrincipal;
if (!principal) {
return null;
}
const isExclusion = lazy.IPPExceptionsManager.hasExclusion(principal);
const isPrivileged = this._isPrivilegedPage(principal);
let siteData = !isPrivileged ? { isExclusion } : null;
return siteData;
}
/**
* BigInts throw when using JSON.stringify or when using arithmetic with
* numbers so we convert them to numbers here so they max and remaining can
* be safely used. Check usageInfo first, if that doesn't exist, check the
* entitlement. If neither exist, return null.
*
* @returns {object} An object with max and remaining as numbers
*/
#getBandwidthUsage() {
if (
lazy.BANDWIDTH_USAGE_ENABLED &&
lazy.IPPProxyManager.usageInfo?.max != null
) {
return {
max: Number(lazy.IPPProxyManager.usageInfo.max),
remaining: Number(lazy.IPPProxyManager.usageInfo.remaining),
reset: lazy.IPPProxyManager.usageInfo.reset,
};
} else if (
lazy.BANDWIDTH_USAGE_ENABLED &&
lazy.IPPEnrollAndEntitleManager.entitlement?.maxBytes != null
) {
// Usage info doesn't exist yet. Check the entitlement
return {
max: Number(lazy.IPPEnrollAndEntitleManager.entitlement?.maxBytes),
remaining: Number(
lazy.IPPEnrollAndEntitleManager.entitlement?.maxBytes
),
reset: null,
};
}
return null;
}
/**
* Checks if the given principal represents a privileged page.
*
* @param {nsIPrincipal} principal
* The principal to evaluate.
* @returns {boolean}
* True if the page is privileged (about: pages or system principal).
*/
_isPrivilegedPage(principal) {
// Ignore about: pages for automated tests, which load in about:blank pages by default.
// Do not register this method as private though so that we can stub it.
return (
(principal.schemeIs("about") || principal.isSystemPrincipal) &&
!Cu.isInAutomation
);
}
/**
* Updates the siteData state property.
*/
#updateSiteData() {
const siteData = this.#getSiteData();
this.setState({ siteData });
}
#handleEvent(event) {
if (event.type == "IPProtection:Init") {
this.updateState();
} else if (event.type == "IPProtection:Close") {
this.close();
} else if (event.type == "IPProtection:UserEnable") {
this.#startProxy();
Services.prefs.setBoolPref("browser.ipProtection.userEnabled", true);
let userEnableCount = Services.prefs.getIntPref(
"browser.ipProtection.userEnableCount",
0
);
if (userEnableCount < 3) {
Services.prefs.setIntPref(
"browser.ipProtection.userEnableCount",
userEnableCount + 1
);
}
} else if (event.type == "IPProtection:UserDisable") {
this.#stopProxy();
Services.prefs.setBoolPref("browser.ipProtection.userEnabled", false);
} else if (event.type == "IPProtection:ClickUpgrade") {
// Let the service know that we tried upgrading at least once
this.initiatedUpgrade = true;
this.close();
} else if (event.type == "IPProtection:OptIn") {
this.enroll({ entrypoint: "vpn_integration_panel" });
} else if (
event.type == "IPPProxyManager:StateChanged" ||
event.type == "IPProtectionService:StateChanged" ||
event.type === "IPPEnrollAndEntitleManager:StateChanged"
) {
let errorType = "";
if (lazy.IPPProxyManager.state === lazy.IPPProxyStates.ERROR) {
errorType = lazy.ERRORS.GENERIC;
}
this.setState({
isSignedOut: !lazy.IPPSignInWatcher.isSignedIn,
unauthenticated:
lazy.IPProtectionService.state ===
lazy.IPProtectionStates.UNAUTHENTICATED,
isProtectionEnabled:
lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVE,
hasUpgraded: lazy.IPPEnrollAndEntitleManager.hasUpgraded,
error: errorType,
isActivating:
lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVATING,
bandwidthUsage: this.#getBandwidthUsage(),
bandwidthWarning:
lazy.IPProtectionService.state === lazy.IPProtectionStates.READY
? this.#shouldShowBandwidthWarning()
: false,
paused: lazy.IPPProxyManager.state === lazy.IPPProxyStates.PAUSED,
});
} else if (event.type == "IPPExceptionsManager:ExclusionChanged") {
this.#updateSiteData();
} else if (event.type == "IPProtection:UserEnableVPNForSite") {
const win = event.target.ownerGlobal;
const principal = win?.gBrowser.contentPrincipal;
lazy.IPPExceptionsManager.setExclusion(principal, false);
Glean.ipprotection.exclusionToggled.record({ excluded: false });
} else if (event.type == "IPProtection:UserDisableVPNForSite") {
const win = event.target.ownerGlobal;
const principal = win?.gBrowser.contentPrincipal;
lazy.IPPExceptionsManager.setExclusion(principal, true);
Glean.ipprotection.exclusionToggled.record({ excluded: true });
} else if (event.type == "IPProtection:DismissBandwidthWarning") {
this.#lastDismissedUsageState = lazy.IPPUsageHelper.state;
this.setState({ bandwidthWarning: false });
} else if (event.type == "IPPProxyManager:UsageChanged") {
const usage = event.detail.usage;
if (
!usage ||
usage.max == null ||
usage.remaining == null ||
!usage.reset
) {
return;
}
const remainingPercent = Number(usage.remaining) / Number(usage.max);
const upsellThreshold = (1 - BANDWIDTH.FIRST_THRESHOLD) * 100;
const firstWarning = (1 - BANDWIDTH.SECOND_THRESHOLD) * 100;
const secondWarning = (1 - BANDWIDTH.THIRD_THRESHOLD) * 100;
let threshold = 0;
if (
remainingPercent <= BANDWIDTH.FIRST_THRESHOLD &&
remainingPercent > BANDWIDTH.SECOND_THRESHOLD
) {
threshold = upsellThreshold;
} else if (
remainingPercent <= BANDWIDTH.SECOND_THRESHOLD &&
remainingPercent > BANDWIDTH.THIRD_THRESHOLD
) {
threshold = firstWarning;
} else if (
remainingPercent > 0 &&
remainingPercent <= BANDWIDTH.THIRD_THRESHOLD
) {
threshold = secondWarning;
} else if (remainingPercent === 0) {
threshold = 100;
}
const lastRecordedThreshold = Services.prefs.getIntPref(
BANDWIDTH_THRESHOLD_PREF,
threshold
);
Services.prefs.setIntPref(BANDWIDTH_THRESHOLD_PREF, threshold);
if (lastRecordedThreshold !== threshold) {
this.#measureBandwidthThreshold(threshold, lastRecordedThreshold);
}
if (lazy.BANDWIDTH_USAGE_ENABLED) {
this.setState({
bandwidthUsage: {
remaining: Number(usage.remaining),
max: Number(usage.max),
reset: usage.reset,
},
});
}
} else if (event.type == "IPPUsageHelper:StateChanged") {
if (lazy.IPPUsageHelper.state === lazy.UsageStates.NONE) {
this.#lastDismissedUsageState = lazy.UsageStates.NONE;
}
this.setState({ bandwidthWarning: this.#shouldShowBandwidthWarning() });
}
}
#measureBandwidthThreshold(threshold, lastRecordedThreshold) {
if (!threshold || threshold == lastRecordedThreshold) {
return;
}
Glean.ipprotection.bandwidthUsedThreshold.record({
percentage: threshold,
});
}
}