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",
CustomizableUI:
"moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
IPProtectionPanel:
"moz-src:///browser/components/ipprotection/IPProtectionPanel.sys.mjs",
IPProtectionService:
"moz-src:///toolkit/components/ipprotection/IPProtectionService.sys.mjs",
IPProtectionToolbarButton:
"moz-src:///browser/components/ipprotection/IPProtectionToolbarButton.sys.mjs",
IPPProxyManager:
"moz-src:///toolkit/components/ipprotection/IPPProxyManager.sys.mjs",
requestIdleCallback: "resource://gre/modules/Timer.sys.mjs",
cancelIdleCallback: "resource://gre/modules/Timer.sys.mjs",
});
const FXA_WIDGET_ID = "fxa-toolbar-menu-button";
const EXT_WIDGET_ID = "unified-extensions-button";
/**
* IPProtectionWidget is the class for the singleton IPProtection.
*
* It is a minimal manager for creating and removing a CustomizableUI widget
* for IP protection features.
*
* It maintains the state of the panels and updates them when the
* panel is shown or hidden.
*/
class IPProtectionWidget {
static WIDGET_ID = "ipprotection-button";
static PANEL_ID = "PanelUI-ipprotection";
static ENABLED_PREF = "browser.ipProtection.enabled";
static ADDED_PREF = "browser.ipProtection.added";
#inited = false;
created = false;
#panels = new WeakMap();
#toolbarButtons = new WeakMap();
constructor() {
this.sendReadyTrigger = this.#sendReadyTrigger.bind(this);
}
/**
* Creates the widget.
*/
init() {
if (this.#inited) {
return;
}
this.#inited = true;
if (!this.created) {
this.#createWidget();
}
lazy.CustomizableUI.addListener(this);
}
/**
* Destroys the widget and prevents any updates.
*/
uninit() {
if (!this.#inited) {
return;
}
this.#destroyWidget();
this.#uninitPanels();
lazy.CustomizableUI.removeListener(this);
this.#inited = false;
}
/**
* Returns the initialization status
*/
get isInitialized() {
return this.#inited;
}
/**
* Creates the CustomizableUI widget.
*/
#createWidget() {
const onViewShowing = this.#onViewShowing.bind(this);
const onViewHiding = this.#onViewHiding.bind(this);
const onBeforeCreated = this.#onBeforeCreated.bind(this);
const onCreated = this.#onCreated.bind(this);
const onDestroyed = this.#onDestroyed.bind(this);
const item = {
id: IPProtectionWidget.WIDGET_ID,
l10nId: "ipprotection-button",
type: "view",
viewId: IPProtectionWidget.PANEL_ID,
onViewShowing,
onViewHiding,
onBeforeCreated,
onCreated,
onDestroyed,
disallowSubView: true, // Bug 2016480 - Keeps the VPN panel as standard panel for the Overflow menu
};
lazy.CustomizableUI.createWidget(item);
this.#placeWidget();
this.created = true;
}
/**
* Places the widget in the nav bar, next to the FxA widget.
*/
#placeWidget() {
let wasAddedToToolbar = Services.prefs.getBoolPref(
IPProtectionWidget.ADDED_PREF,
false
);
let alreadyPlaced = lazy.CustomizableUI.getPlacementOfWidget(
IPProtectionWidget.WIDGET_ID,
false,
true
);
if (wasAddedToToolbar || alreadyPlaced) {
return;
}
let prevWidget =
lazy.CustomizableUI.getPlacementOfWidget(FXA_WIDGET_ID) ||
lazy.CustomizableUI.getPlacementOfWidget(EXT_WIDGET_ID);
let pos = prevWidget ? prevWidget.position : null;
lazy.CustomizableUI.addWidgetToArea(
IPProtectionWidget.WIDGET_ID,
lazy.CustomizableUI.AREA_NAVBAR,
pos
);
Services.prefs.setBoolPref(IPProtectionWidget.ADDED_PREF, true);
}
/**
* Destroys the widget if it has been created.
*
* This will not remove the pref listeners, so the widget
* can be recreated later.
*/
#destroyWidget() {
if (!this.created) {
return;
}
this.#destroyPanels();
lazy.CustomizableUI.destroyWidget(IPProtectionWidget.WIDGET_ID);
this.created = false;
if (this.readyTriggerIdleCallback) {
lazy.cancelIdleCallback(this.readyTriggerIdleCallback);
}
}
/**
* Get the IPProtectionPanel for a given window.
*
* @param {Window} window - which window to get the panel for.
* @returns {IPProtectionPanel}
*/
getPanel(window) {
if (!this.created || !window?.PanelUI) {
return null;
}
return this.#panels.get(window);
}
/**
* Get the IPProtectionToolbarButton for a given window.
*
* @param {Window} window - which window to get the toolbar button for.
* @returns {IPProtectionToolbarButton}
*/
getToolbarButton(window) {
if (!this.created) {
return null;
}
return this.#toolbarButtons.get(window);
}
/**
* Remove all panels content, but maintains state for if the widget is
* re-enabled in the same window.
*
* Panels will only be removed from the WeakMap if their window is closed.
*/
#destroyPanels() {
let panels = ChromeUtils.nondeterministicGetWeakMapKeys(this.#panels);
for (let panel of panels) {
this.#panels.get(panel).destroy();
}
}
/**
* Uninit all panels and toolbar buttons and clear the WeakMaps.
*/
#uninitPanels() {
let panels = ChromeUtils.nondeterministicGetWeakMapKeys(this.#panels);
for (let panel of panels) {
this.#panels.get(panel).uninit();
}
let toolbarButtons = ChromeUtils.nondeterministicGetWeakMapKeys(
this.#toolbarButtons
);
for (let toolbarButton of toolbarButtons) {
this.#toolbarButtons.get(toolbarButton).uninit();
}
this.#panels = new WeakMap();
this.#toolbarButtons = new WeakMap();
}
/**
* Updates the state of the panel before it is shown.
*
* @param {Event} event - the panel shown.
*/
#onViewShowing(event) {
let { ownerGlobal } = event.target;
if (this.#panels.has(ownerGlobal)) {
let panel = this.#panels.get(ownerGlobal);
panel.showing(event.target);
}
}
/**
* Updates the panels visibility.
*
* @param {Event} event - the panel hidden.
*/
#onViewHiding(event) {
let { ownerGlobal } = event.target;
if (this.#panels.has(ownerGlobal)) {
let panel = this.#panels.get(ownerGlobal);
panel.hiding();
}
}
/**
* Creates a new IPProtectionPanel for a browser window.
*
* @param {Document} doc - the document containing the panel.
*/
#onBeforeCreated(doc) {
let { ownerGlobal } = doc;
if (ownerGlobal && !this.#panels.has(ownerGlobal)) {
let panel = new lazy.IPProtectionPanel(ownerGlobal, this.variant);
this.#panels.set(ownerGlobal, panel);
}
}
/**
* Gets the toolbaritem after the widget has been created,
* creates the toolbar button with initial state, and adds content to the panel.
*
* @param {XULElement} toolbaritem - the widget toolbaritem.
*/
#onCreated(toolbaritem) {
let window = toolbaritem.ownerGlobal;
if (window && !this.#toolbarButtons.has(window)) {
let toolbarButton = new lazy.IPProtectionToolbarButton(
window,
IPProtectionWidget.WIDGET_ID,
toolbaritem
);
this.#toolbarButtons.set(window, toolbarButton);
}
this.readyTriggerIdleCallback = lazy.requestIdleCallback(
this.sendReadyTrigger
);
lazy.IPProtectionService.addEventListener(
"IPProtectionService:StateChanged",
this.handleEvent
);
lazy.IPPProxyManager.addEventListener(
"IPPProxyManager:StateChanged",
this.handleEvent
);
}
#onDestroyed() {
lazy.IPPProxyManager.removeEventListener(
"IPPProxyManager:StateChanged",
this.handleEvent
);
lazy.IPProtectionService.removeEventListener(
"IPProtectionService:StateChanged",
this.handleEvent
);
}
async onWidgetRemoved(widgetId) {
if (widgetId != IPProtectionWidget.WIDGET_ID) {
return;
}
// Shut down VPN connection when widget is removed,
// but wait to check if it has been moved.
await Promise.resolve();
let moved = !!lazy.CustomizableUI.getPlacementOfWidget(widgetId);
if (!moved) {
Glean.ipprotection.removedFromToolbar.record();
lazy.IPPProxyManager.stop();
}
}
async #sendReadyTrigger() {
await lazy.ASRouter.waitForInitialized;
const win = Services.wm.getMostRecentBrowserWindow();
const browser = win?.gBrowser?.selectedBrowser;
await lazy.ASRouter.sendTriggerMessage({
browser,
id: "ipProtectionReady",
});
}
}
const IPProtection = new IPProtectionWidget();
export { IPProtection, IPProtectionWidget };