IPProtection.sys.mjs (10377B)
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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 import { ERRORS } from "chrome://browser/content/ipprotection/ipprotection-constants.mjs"; 7 8 const lazy = {}; 9 10 ChromeUtils.defineESModuleGetters(lazy, { 11 ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs", 12 CustomizableUI: 13 "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs", 14 IPProtectionPanel: 15 "moz-src:///browser/components/ipprotection/IPProtectionPanel.sys.mjs", 16 IPProtectionService: 17 "moz-src:///browser/components/ipprotection/IPProtectionService.sys.mjs", 18 IPProtectionStates: 19 "moz-src:///browser/components/ipprotection/IPProtectionService.sys.mjs", 20 IPPProxyManager: 21 "moz-src:///browser/components/ipprotection/IPPProxyManager.sys.mjs", 22 IPPProxyStates: 23 "moz-src:///browser/components/ipprotection/IPPProxyManager.sys.mjs", 24 requestIdleCallback: "resource://gre/modules/Timer.sys.mjs", 25 cancelIdleCallback: "resource://gre/modules/Timer.sys.mjs", 26 }); 27 28 const FXA_WIDGET_ID = "fxa-toolbar-menu-button"; 29 const EXT_WIDGET_ID = "unified-extensions-button"; 30 31 /** 32 * IPProtectionWidget is the class for the singleton IPProtection. 33 * 34 * It is a minimal manager for creating and removing a CustomizableUI widget 35 * for IP protection features. 36 * 37 * It maintains the state of the panels and updates them when the 38 * panel is shown or hidden. 39 */ 40 class IPProtectionWidget { 41 static WIDGET_ID = "ipprotection-button"; 42 static PANEL_ID = "PanelUI-ipprotection"; 43 44 static ENABLED_PREF = "browser.ipProtection.enabled"; 45 static VARIANT_PREF = "browser.ipProtection.variant"; 46 static ADDED_PREF = "browser.ipProtection.added"; 47 48 #inited = false; 49 created = false; 50 #panels = new WeakMap(); 51 52 constructor() { 53 this.sendReadyTrigger = this.#sendReadyTrigger.bind(this); 54 this.handleEvent = this.#handleEvent.bind(this); 55 } 56 57 /** 58 * Creates the widget. 59 */ 60 init() { 61 if (this.#inited) { 62 return; 63 } 64 this.#inited = true; 65 66 if (!this.created) { 67 this.#createWidget(); 68 } 69 70 lazy.CustomizableUI.addListener(this); 71 } 72 73 /** 74 * Destroys the widget and prevents any updates. 75 */ 76 uninit() { 77 if (!this.#inited) { 78 return; 79 } 80 this.#destroyWidget(); 81 this.#uninitPanels(); 82 83 lazy.CustomizableUI.removeListener(this); 84 85 this.#inited = false; 86 } 87 88 /** 89 * Returns the initialization status 90 */ 91 get isInitialized() { 92 return this.#inited; 93 } 94 95 /** 96 * Updates the toolbar icon to reflect the VPN connection status 97 * 98 * @param {XULElement} toolbaritem - toolbaritem to update 99 * @param {object} status - VPN connection status 100 */ 101 updateIconStatus(toolbaritem, status = { isActive: false, isError: false }) { 102 let isActive = status.isActive; 103 let isError = status.isError; 104 let l10nId = isError ? "ipprotection-button-error" : "ipprotection-button"; 105 106 if (isError) { 107 toolbaritem.classList.remove("ipprotection-on"); 108 toolbaritem.classList.add("ipprotection-error"); 109 } else if (isActive) { 110 toolbaritem.classList.remove("ipprotection-error"); 111 toolbaritem.classList.add("ipprotection-on"); 112 } else { 113 toolbaritem.classList.remove("ipprotection-error"); 114 toolbaritem.classList.remove("ipprotection-on"); 115 } 116 117 toolbaritem.setAttribute("data-l10n-id", l10nId); 118 } 119 120 /** 121 * Creates the CustomizableUI widget. 122 */ 123 #createWidget() { 124 const onViewShowing = this.#onViewShowing.bind(this); 125 const onViewHiding = this.#onViewHiding.bind(this); 126 const onBeforeCreated = this.#onBeforeCreated.bind(this); 127 const onCreated = this.#onCreated.bind(this); 128 const onDestroyed = this.#onDestroyed.bind(this); 129 const item = { 130 id: IPProtectionWidget.WIDGET_ID, 131 l10nId: "ipprotection-button", 132 type: "view", 133 viewId: IPProtectionWidget.PANEL_ID, 134 onViewShowing, 135 onViewHiding, 136 onBeforeCreated, 137 onCreated, 138 onDestroyed, 139 }; 140 lazy.CustomizableUI.createWidget(item); 141 142 this.#placeWidget(); 143 144 this.created = true; 145 } 146 147 /** 148 * Places the widget in the nav bar, next to the FxA widget. 149 */ 150 #placeWidget() { 151 let wasAddedToToolbar = Services.prefs.getBoolPref( 152 IPProtectionWidget.ADDED_PREF, 153 false 154 ); 155 let alreadyPlaced = lazy.CustomizableUI.getPlacementOfWidget( 156 IPProtectionWidget.WIDGET_ID, 157 false, 158 true 159 ); 160 if (wasAddedToToolbar || alreadyPlaced) { 161 return; 162 } 163 164 let prevWidget = 165 lazy.CustomizableUI.getPlacementOfWidget(FXA_WIDGET_ID) || 166 lazy.CustomizableUI.getPlacementOfWidget(EXT_WIDGET_ID); 167 let pos = prevWidget ? prevWidget.position - 1 : null; 168 169 lazy.CustomizableUI.addWidgetToArea( 170 IPProtectionWidget.WIDGET_ID, 171 lazy.CustomizableUI.AREA_NAVBAR, 172 pos 173 ); 174 Services.prefs.setBoolPref(IPProtectionWidget.ADDED_PREF, true); 175 } 176 177 /** 178 * Destroys the widget if it has been created. 179 * 180 * This will not remove the pref listeners, so the widget 181 * can be recreated later. 182 */ 183 #destroyWidget() { 184 if (!this.created) { 185 return; 186 } 187 this.#destroyPanels(); 188 lazy.CustomizableUI.destroyWidget(IPProtectionWidget.WIDGET_ID); 189 this.created = false; 190 if (this.readyTriggerIdleCallback) { 191 lazy.cancelIdleCallback(this.readyTriggerIdleCallback); 192 } 193 } 194 195 /** 196 * Get the IPProtectionPanel for q given window. 197 * 198 * @param {Window} window - which window to get the panel for. 199 * @returns {IPProtectionPanel} 200 */ 201 getPanel(window) { 202 if (!this.created || !window?.PanelUI) { 203 return null; 204 } 205 206 return this.#panels.get(window); 207 } 208 209 /** 210 * Remove all panels content, but maintains state for if the widget is 211 * re-enabled in the same window. 212 * 213 * Panels will only be removed from the WeakMap if their window is closed. 214 */ 215 #destroyPanels() { 216 let panels = ChromeUtils.nondeterministicGetWeakMapKeys(this.#panels); 217 for (let panel of panels) { 218 this.#panels.get(panel).destroy(); 219 } 220 } 221 222 /** 223 * Uninit all panels and clear the WeakMap. 224 */ 225 #uninitPanels() { 226 let panels = ChromeUtils.nondeterministicGetWeakMapKeys(this.#panels); 227 for (let panel of panels) { 228 this.#panels.get(panel).uninit(); 229 } 230 this.#panels = new WeakMap(); 231 } 232 233 /** 234 * Updates the state of the panel before it is shown. 235 * 236 * @param {Event} event - the panel shown. 237 */ 238 #onViewShowing(event) { 239 let { ownerGlobal } = event.target; 240 if (this.#panels.has(ownerGlobal)) { 241 let panel = this.#panels.get(ownerGlobal); 242 panel.showing(event.target); 243 } 244 } 245 246 /** 247 * Updates the panels visibility. 248 * 249 * @param {Event} event - the panel hidden. 250 */ 251 #onViewHiding(event) { 252 let { ownerGlobal } = event.target; 253 if (this.#panels.has(ownerGlobal)) { 254 let panel = this.#panels.get(ownerGlobal); 255 panel.hiding(); 256 } 257 } 258 259 /** 260 * Creates a new IPProtectionPanel for a browser window. 261 * 262 * @param {Document} doc - the document containing the panel. 263 */ 264 #onBeforeCreated(doc) { 265 let { ownerGlobal } = doc; 266 if (ownerGlobal && !this.#panels.has(ownerGlobal)) { 267 let panel = new lazy.IPProtectionPanel(ownerGlobal, this.variant); 268 this.#panels.set(ownerGlobal, panel); 269 } 270 } 271 272 /** 273 * Gets the toolbaritem after the widget has been created and 274 * adds content to the panel. 275 * 276 * @param {XULElement} toolbaritem - the widget toolbaritem. 277 */ 278 #onCreated(toolbaritem) { 279 let isActive = lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVE; 280 let isError = 281 lazy.IPPProxyManager.state === lazy.IPPProxyStates.ERROR && 282 lazy.IPPProxyManager.errors.includes(ERRORS.GENERIC); 283 this.updateIconStatus(toolbaritem, { 284 isActive, 285 isError, 286 }); 287 288 this.readyTriggerIdleCallback = lazy.requestIdleCallback( 289 this.sendReadyTrigger 290 ); 291 292 lazy.IPProtectionService.addEventListener( 293 "IPProtectionService:StateChanged", 294 this.handleEvent 295 ); 296 lazy.IPPProxyManager.addEventListener( 297 "IPPProxyManager:StateChanged", 298 this.handleEvent 299 ); 300 } 301 302 #onDestroyed() { 303 lazy.IPPProxyManager.removeEventListener( 304 "IPPProxyManager:StateChanged", 305 this.handleEvent 306 ); 307 lazy.IPProtectionService.removeEventListener( 308 "IPProtectionService:StateChanged", 309 this.handleEvent 310 ); 311 } 312 313 async onWidgetRemoved(widgetId) { 314 if (widgetId != IPProtectionWidget.WIDGET_ID) { 315 return; 316 } 317 318 // Shut down VPN connection when widget is removed, 319 // but wait to check if it has been moved. 320 await Promise.resolve(); 321 let moved = !!lazy.CustomizableUI.getPlacementOfWidget(widgetId); 322 if (!moved) { 323 lazy.IPPProxyManager.stop(); 324 } 325 } 326 327 async #sendReadyTrigger() { 328 await lazy.ASRouter.waitForInitialized; 329 const win = Services.wm.getMostRecentBrowserWindow(); 330 const browser = win?.gBrowser?.selectedBrowser; 331 await lazy.ASRouter.sendTriggerMessage({ 332 browser, 333 id: "ipProtectionReady", 334 }); 335 } 336 337 #handleEvent(event) { 338 if ( 339 event.type == "IPProtectionService:StateChanged" || 340 event.type == "IPPProxyManager:StateChanged" 341 ) { 342 if ( 343 lazy.IPProtectionService.state === lazy.IPProtectionStates.OPTED_OUT 344 ) { 345 lazy.CustomizableUI.removeWidgetFromArea(IPProtectionWidget.WIDGET_ID); 346 return; 347 } 348 349 let status = { 350 isActive: lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVE, 351 isError: 352 lazy.IPPProxyManager.state === lazy.IPPProxyStates.ERROR && 353 lazy.IPPProxyManager.errors.includes(ERRORS.GENERIC), 354 }; 355 356 let widget = lazy.CustomizableUI.getWidget(IPProtectionWidget.WIDGET_ID); 357 let windows = ChromeUtils.nondeterministicGetWeakMapKeys(this.#panels); 358 for (let win of windows) { 359 let toolbaritem = widget.forWindow(win).node; 360 this.updateIconStatus(toolbaritem, status); 361 } 362 } 363 } 364 } 365 366 const IPProtection = new IPProtectionWidget(); 367 368 XPCOMUtils.defineLazyPreferenceGetter( 369 IPProtection, 370 "variant", 371 IPProtectionWidget.VARIANT_PREF, 372 "" 373 ); 374 375 export { IPProtection, IPProtectionWidget };