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/. */
import { ERRORS } from "chrome://browser/content/ipprotection/ipprotection-constants.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
CustomizableUI:
"moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
IPPExceptionsManager:
"moz-src:///browser/components/ipprotection/IPPExceptionsManager.sys.mjs",
IPPNetworkUtils:
"moz-src:///browser/components/ipprotection/IPPNetworkUtils.sys.mjs",
IPPProxyManager:
"moz-src:///browser/components/ipprotection/IPPProxyManager.sys.mjs",
IPProtectionService:
"moz-src:///browser/components/ipprotection/IPProtectionService.sys.mjs",
IPPProxyStates:
"moz-src:///browser/components/ipprotection/IPPProxyManager.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"siteExceptionsFeaturePref",
"browser.ipProtection.features.siteExceptions",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"siteExceptionsHintsPref",
"browser.ipProtection.siteExceptionsHintsEnabled",
true
);
/**
* IPProtectionToolbarButton manages the IP Protection toolbar button
* for a single browser window.
*
* Each instance:
* - Tracks location changes via a progress listener
* - Updates the button icon according to the proxy state, proxy errors,
* offline status, and site exclusions
* - Handles the visual state of the toolbar button
*/
export class IPProtectionToolbarButton {
#window = null;
#progressListener = null;
#widgetId = null;
#previousIsExcluded = null;
static CONFIRMATION_HINT_MESSAGE_ID =
"confirmation-hint-ipprotection-navigated-to-excluded-site";
/**
* 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 value of the pref
* browser.ipProtection.features.siteExceptions.
*
* @returns {boolean}
* True if site exceptions support is enabled, false otherwise.
*/
get isExceptionsFeatureEnabled() {
return lazy.siteExceptionsFeaturePref;
}
/**
* Gets the value of the pref
* browser.ipProtection.siteExceptionsHintsEnabled.
*
* @returns {boolean}
* True if confirmation hints for site exceptions are enabled, false otherwise.
*/
get isExceptionsHintsEnabled() {
return lazy.siteExceptionsHintsPref;
}
/**
* Gets the toolbaritem for this window.
*
* @returns {XULElement|null}
* The toolbaritem element, or null if not available.
*/
get toolbaritem() {
const win = this.#window.get();
if (!win) {
return null;
}
return lazy.CustomizableUI.getWidget(this.#widgetId)?.forWindow(win).node;
}
constructor(window, widgetId, toolbaritem = null) {
this.#window = Cu.getWeakReference(window);
this.#widgetId = widgetId;
this.handleEvent = this.#handleEvent.bind(this);
this.observeOfflineStatus = this.#observeOfflineStatus.bind(this);
this.#addProgressListener();
lazy.IPProtectionService.addEventListener(
"IPProtectionService:StateChanged",
this.handleEvent
);
lazy.IPPProxyManager.addEventListener(
"IPPProxyManager:StateChanged",
this.handleEvent
);
lazy.IPPExceptionsManager.addEventListener(
"IPPExceptionsManager:ExclusionChanged",
this.handleEvent
);
Services.obs.addObserver(
this.observeOfflineStatus,
"network:offline-status-changed"
);
if (this.gBrowser?.tabContainer) {
this.gBrowser.tabContainer.addEventListener("TabSelect", this);
}
if (toolbaritem) {
toolbaritem.classList.add("subviewbutton-nav"); // adds the right arrow in overflow menu
this.updateState(toolbaritem);
}
}
/**
* Creates and registers a progress listener for the window.
*/
#addProgressListener() {
if (!this.gBrowser) {
return;
}
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 (!aLocationURI) {
return;
}
const isReload =
aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD;
this.updateState(null, { showConfirmationHint: !isReload });
},
};
this.gBrowser.addTabsProgressListener(this.#progressListener);
}
/**
* Event handler for document-level events.
*
* @param {Event} event
* The event to handle.
*/
#handleEvent(event) {
if (
event.type === "IPProtectionService:StateChanged" ||
event.type === "IPPProxyManager:StateChanged" ||
event.type === "IPPExceptionsManager:ExclusionChanged"
) {
this.updateState();
} else if (event.type === "TabSelect") {
this.updateState();
}
}
/**
* Observer for network offline status changes.
* Updates the state for every change in case we need to show a different icon.
*
* @param {nsISupports} _subject
* @param {string} topic
* @param {string} _data
*/
#observeOfflineStatus(_subject, topic, _data) {
if (topic === "network:offline-status-changed") {
this.updateState();
}
}
/**
* Updates the button to reflect the current state.
*
* This method is called under these circumstances:
* 1. After creating the toolbar button, to set up the initial icon
* 2. After an IPProtectionService or IPPProxyManager state change
* 3. After pressing the site exclusion toggle on the panel and the
* exclusion state for a site has changed in ipp-vpn
* 4. After a location change / page navigation
* 5. After tab switching
* 6. After offline network status changes
*
* @param {XULElement|null} [toolbaritem]
* Optional toolbaritem to update directly.
* If not provided, looks up the toolbaritem via CustomizableUI.
* If provided, but toolbaritem is null, this means the toolbaritem isn't available yet.
* @param {object} [options]
* Optional options object
* @param {boolean} [options.showConfirmationHint=true]
* Whether to show confirmation hints for navigation to excluded sites
*/
updateState(toolbaritem = null, options = { showConfirmationHint: true }) {
const win = this.#window.get();
if (!win) {
return;
}
toolbaritem ??= this.toolbaritem;
if (!toolbaritem) {
return;
}
// Check the ipp-vpn permission using IPPExceptionsManager.
let principal = this.gBrowser?.contentPrincipal;
let isExcluded = this.#isExcludedSite(principal);
let isActive = lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVE;
// Show error icon when proxy manager is in ERROR state or when offline
let hasProxyError =
lazy.IPPProxyManager.state === lazy.IPPProxyStates.ERROR &&
(lazy.IPPProxyManager.errors.includes(ERRORS.GENERIC) ||
lazy.IPPProxyManager.errors.includes(ERRORS.NETWORK));
let isOffline = lazy.IPPNetworkUtils.isOffline;
let isError = hasProxyError || isOffline;
const showConfirmationHint = options.showConfirmationHint ?? true;
if (showConfirmationHint) {
this.updateConfirmationHint(win.ConfirmationHint, toolbaritem, {
isActive,
isError,
isExcluded,
});
}
// Null principals reset the previous state to false if
// the state was initially true. To avoid this, only set
// the previous state if not a null principal.
if (principal && !principal.isNullPrincipal) {
this.#previousIsExcluded = isExcluded;
}
this.updateIconStatus(toolbaritem, {
isActive,
isError,
isExcluded,
});
}
/**
* Shows a confirmation hint after navigating from a
* protected site to an excluded site while the VPN is on.
* Ignore the message if there is an error or the VPN is off.
*
* @param {object} confirmationHint
* The current window's confirmation hint instance
* @param {XULElement} toolbaritem
* The toolbaritem to anchor the confirmation hint to
* @param {object} status
* VPN connection status
*/
updateConfirmationHint(
confirmationHint,
toolbaritem,
status = { isActive: false, isError: false, isExcluded: false }
) {
if (!confirmationHint) {
return;
}
let exceptionsPrefsEnabled =
this.isExceptionsFeatureEnabled && this.isExceptionsHintsEnabled;
const canShowConfirmationHint =
exceptionsPrefsEnabled &&
!status.isError &&
status.isActive &&
status.isExcluded &&
!this.#previousIsExcluded;
if (!canShowConfirmationHint) {
return;
}
confirmationHint.show(
toolbaritem,
IPProtectionToolbarButton.CONFIRMATION_HINT_MESSAGE_ID,
{
position: "bottomright topright", // panel anchor, message anchor
}
);
}
/**
* Updates the toolbar button icon to reflect the VPN connection status
*
* @param {XULElement} toolbaritem
* The toolbaritem to update
* @param {object} status
* VPN connection status
*/
updateIconStatus(
toolbaritem,
status = { isActive: false, isError: false, isExcluded: false }
) {
if (!toolbaritem) {
return;
}
let isActive = status.isActive;
let isError = status.isError;
let isExcluded = status.isExcluded && this.isExceptionsFeatureEnabled;
let l10nId = isError ? "ipprotection-button-error" : "ipprotection-button";
toolbaritem.classList.remove(
"ipprotection-on",
"ipprotection-error",
"ipprotection-excluded"
);
if (isError) {
toolbaritem.classList.add("ipprotection-error");
} else if (isExcluded && isActive) {
toolbaritem.classList.add("ipprotection-excluded");
} else if (isActive) {
toolbaritem.classList.add("ipprotection-on");
}
toolbaritem.setAttribute("data-l10n-id", l10nId);
}
/**
* Checks if the given principal is excluded from IP Protection.
*
* @param {nsIPrincipal} principal
* The principal to check.
* @returns {boolean}
* True if the site is excluded, false otherwise.
*/
#isExcludedSite(principal) {
if (!principal || principal.isNullPrincipal) {
return false;
}
return lazy.IPPExceptionsManager.hasExclusion(principal);
}
/**
* Cleans up listeners and observers when the button is destroyed.
*/
uninit() {
if (this.gBrowser && this.#progressListener) {
this.gBrowser.removeTabsProgressListener(this.#progressListener);
}
this.#progressListener = null;
if (this.gBrowser?.tabContainer) {
this.gBrowser.tabContainer.removeEventListener("TabSelect", this);
}
lazy.IPProtectionService.removeEventListener(
"IPProtectionService:StateChanged",
this.handleEvent
);
lazy.IPPProxyManager.removeEventListener(
"IPPProxyManager:StateChanged",
this.handleEvent
);
lazy.IPPExceptionsManager.removeEventListener(
"IPPExceptionsManager:ExclusionChanged",
this.handleEvent
);
Services.obs.removeObserver(
this.observeOfflineStatus,
"network:offline-status-changed"
);
}
}