AccountsGlue.sys.mjs (15468B)
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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 7 8 const lazy = {}; 9 10 ChromeUtils.defineESModuleGetters(lazy, { 11 AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs", 12 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 13 BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs", 14 ClientID: "resource://gre/modules/ClientID.sys.mjs", 15 CloseRemoteTab: "resource://gre/modules/FxAccountsCommands.sys.mjs", 16 FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs", 17 UIState: "resource://services-sync/UIState.sys.mjs", 18 }); 19 20 XPCOMUtils.defineLazyPreferenceGetter( 21 lazy, 22 "CLIENT_ASSOCIATION_PING_ENABLED", 23 "identity.fxaccounts.telemetry.clientAssociationPing.enabled", 24 false 25 ); 26 27 XPCOMUtils.defineLazyServiceGetter( 28 lazy, 29 "AlertsService", 30 "@mozilla.org/alerts-service;1", 31 Ci.nsIAlertsService 32 ); 33 34 ChromeUtils.defineLazyGetter( 35 lazy, 36 "accountsL10n", 37 () => new Localization(["browser/accounts.ftl", "branding/brand.ftl"], true) 38 ); 39 40 const AlertNotification = Components.Constructor( 41 "@mozilla.org/alert-notification;1", 42 "nsIAlertNotification", 43 "initWithObject" 44 ); 45 46 /** 47 * Manages Mozilla Account and Sync related functionality 48 * needed at startup. It mainly handles various account-related events and notifications. 49 * 50 * This module was sliced off of BrowserGlue and designed to centralize 51 * account-related events/notifications to prevent crowding BrowserGlue 52 */ 53 export const AccountsGlue = { 54 QueryInterface: ChromeUtils.generateQI([ 55 "nsIObserver", 56 "nsISupportsWeakReference", 57 ]), 58 59 init() { 60 let os = Services.obs; 61 [ 62 "fxaccounts:onverified", 63 "fxaccounts:device_connected", 64 "fxaccounts:verify_login", 65 "fxaccounts:device_disconnected", 66 "fxaccounts:commands:open-uri", 67 "fxaccounts:commands:close-uri", 68 "sync-ui-state:update", 69 ].forEach(topic => os.addObserver(this, topic, true)); 70 }, 71 72 observe(subject, topic, data) { 73 switch (topic) { 74 case "fxaccounts:onverified": 75 this._onThisDeviceConnected(); 76 break; 77 case "fxaccounts:device_connected": 78 this._onDeviceConnected(data); 79 break; 80 case "fxaccounts:verify_login": 81 this._onVerifyLoginNotification(JSON.parse(data)); 82 break; 83 case "fxaccounts:device_disconnected": 84 data = JSON.parse(data); 85 if (data.isLocalDevice) { 86 this._onDeviceDisconnected(); 87 } 88 break; 89 case "fxaccounts:commands:open-uri": 90 this._onDisplaySyncURIs(subject); 91 break; 92 case "fxaccounts:commands:close-uri": 93 this._onIncomingCloseTabCommand(subject); 94 break; 95 case "sync-ui-state:update": { 96 this._updateFxaBadges( 97 lazy.BrowserWindowTracker.getTopWindow({ 98 allowFromInactiveWorkspace: true, 99 }) 100 ); 101 102 if (lazy.CLIENT_ASSOCIATION_PING_ENABLED) { 103 let fxaState = lazy.UIState.get(); 104 if (fxaState.status == lazy.UIState.STATUS_SIGNED_IN) { 105 Glean.clientAssociation.uid.set(fxaState.uid); 106 Glean.clientAssociation.legacyClientId.set( 107 lazy.ClientID.getCachedClientID() 108 ); 109 } 110 } 111 break; 112 } 113 case "browser-glue-test": // used by tests 114 if (data == "mock-alerts-service") { 115 // eslint-disable-next-line mozilla/valid-lazy 116 Object.defineProperty(lazy, "AlertsService", { 117 value: subject.wrappedJSObject, 118 }); 119 } 120 break; 121 } 122 }, 123 124 _onThisDeviceConnected() { 125 const [title, body] = lazy.accountsL10n.formatValuesSync([ 126 "account-connection-title-2", 127 "account-connection-connected", 128 ]); 129 130 let clickCallback = (subject, topic) => { 131 if (topic != "alertclickcallback") { 132 return; 133 } 134 this._openPreferences("sync"); 135 }; 136 let alert = new AlertNotification({ 137 title, 138 text: body, 139 textClickable: true, 140 }); 141 lazy.AlertsService.showAlert(alert, clickCallback); 142 }, 143 144 _openURLInNewWindow(url) { 145 let urlString = Cc["@mozilla.org/supports-string;1"].createInstance( 146 Ci.nsISupportsString 147 ); 148 urlString.data = url; 149 return new Promise(resolve => { 150 let win = Services.ww.openWindow( 151 null, 152 AppConstants.BROWSER_CHROME_URL, 153 "_blank", 154 "chrome,all,dialog=no", 155 urlString 156 ); 157 win.addEventListener( 158 "load", 159 () => { 160 resolve(win); 161 }, 162 { once: true } 163 ); 164 }); 165 }, 166 167 /** 168 * Called as an observer when Sync's "display URIs" notification is fired. 169 * We open the received URIs in background tabs. 170 * 171 * @param {object} data 172 * The data passed to the observer notification, which contains 173 * a wrappedJSObject with the URIs to open. 174 */ 175 async _onDisplaySyncURIs(data) { 176 try { 177 // The payload is wrapped weirdly because of how Sync does notifications. 178 const URIs = data.wrappedJSObject.object; 179 180 // win can be null, but it's ok, we'll assign it later in openTab() 181 let win = lazy.BrowserWindowTracker.getTopWindow({ private: false }); 182 183 const openTab = async URI => { 184 let tab; 185 if (!win) { 186 win = await this._openURLInNewWindow(URI.uri); 187 let tabs = win.gBrowser.tabs; 188 tab = tabs[tabs.length - 1]; 189 } else { 190 tab = win.gBrowser.addWebTab(URI.uri); 191 } 192 tab.attention = true; 193 return tab; 194 }; 195 196 const firstTab = await openTab(URIs[0]); 197 await Promise.all(URIs.slice(1).map(URI => openTab(URI))); 198 199 const deviceName = URIs[0].sender && URIs[0].sender.name; 200 let titleL10nId, body; 201 if (URIs.length == 1) { 202 // Due to bug 1305895, tabs from iOS may not have device information, so 203 // we have separate strings to handle those cases. (See Also 204 // unnamedTabsArrivingNotificationNoDevice.body below) 205 titleL10nId = deviceName 206 ? { 207 id: "account-single-tab-arriving-from-device-title", 208 args: { deviceName }, 209 } 210 : { id: "account-single-tab-arriving-title" }; 211 // Use the page URL as the body. We strip the fragment and query (after 212 // the `?` and `#` respectively) to reduce size, and also format it the 213 // same way that the url bar would. 214 let url = URIs[0].uri.replace(/([?#]).*$/, "$1"); 215 const wasTruncated = url.length < URIs[0].uri.length; 216 url = lazy.BrowserUIUtils.trimURL(url); 217 if (wasTruncated) { 218 body = await lazy.accountsL10n.formatValue( 219 "account-single-tab-arriving-truncated-url", 220 { url } 221 ); 222 } else { 223 body = url; 224 } 225 } else { 226 titleL10nId = { id: "account-multiple-tabs-arriving-title" }; 227 const allKnownSender = URIs.every(URI => URI.sender != null); 228 const allSameDevice = 229 allKnownSender && 230 URIs.every(URI => URI.sender.id == URIs[0].sender.id); 231 let bodyL10nId; 232 if (allSameDevice) { 233 bodyL10nId = deviceName 234 ? "account-multiple-tabs-arriving-from-single-device" 235 : "account-multiple-tabs-arriving-from-unknown-device"; 236 } else { 237 bodyL10nId = "account-multiple-tabs-arriving-from-multiple-devices"; 238 } 239 240 body = await lazy.accountsL10n.formatValue(bodyL10nId, { 241 deviceName, 242 tabCount: URIs.length, 243 }); 244 } 245 const title = await lazy.accountsL10n.formatValue(titleL10nId); 246 247 const clickCallback = (obsSubject, obsTopic) => { 248 if (obsTopic == "alertclickcallback") { 249 win.gBrowser.selectedTab = firstTab; 250 } 251 }; 252 253 let alert = new AlertNotification({ 254 title, 255 text: body, 256 textClickable: true, 257 }); 258 lazy.AlertsService.showAlert(alert, clickCallback); 259 } catch (ex) { 260 console.error("Error displaying tab(s) received by Sync: ", ex); 261 } 262 }, 263 264 async _onIncomingCloseTabCommand(data) { 265 // The payload is wrapped weirdly because of how Sync does notifications. 266 const wrappedObj = data.wrappedJSObject.object; 267 let { urls } = wrappedObj[0]; 268 let urisToClose = []; 269 urls.forEach(urlString => { 270 try { 271 urisToClose.push(Services.io.newURI(urlString)); 272 } catch (ex) { 273 // The url was invalid so we ignore 274 console.error(ex); 275 } 276 }); 277 // We want to keep track of the tabs we closed for the notification 278 // given that there could be duplicates we also closed 279 let totalClosedTabs = 0; 280 const windows = lazy.BrowserWindowTracker.orderedWindows; 281 282 async function closeTabsInWindows() { 283 for (const win of windows) { 284 if (!win.gBrowser) { 285 continue; 286 } 287 try { 288 const closedInWindow = await win.gBrowser.closeTabsByURI(urisToClose); 289 totalClosedTabs += closedInWindow; 290 } catch (ex) { 291 this.log.error("Error closing tabs in window:", ex); 292 } 293 } 294 } 295 296 await closeTabsInWindows(); 297 298 let clickCallback = async (subject, topic) => { 299 if (topic == "alertshow") { 300 // Keep track of the fact that we showed the notification to 301 // the user at least once 302 lazy.CloseRemoteTab.hasPendingCloseTabNotification = true; 303 } 304 305 // The notification is either turned off or dismissed by user 306 if (topic == "alertfinished") { 307 // Reset the notification pending flag 308 lazy.CloseRemoteTab.hasPendingCloseTabNotification = false; 309 } 310 311 if (topic != "alertclickcallback") { 312 return; 313 } 314 let win = 315 lazy.BrowserWindowTracker.getTopWindow({ private: false }) ?? 316 (await lazy.BrowserWindowTracker.promiseOpenWindow()); 317 // We don't want to open a new tab, instead use the handler 318 // to switch to the existing view 319 if (win) { 320 win.FirefoxViewHandler.openTab("recentlyclosed"); 321 } 322 }; 323 324 // Reset the count only if there are no pending notifications 325 if (!lazy.CloseRemoteTab.hasPendingCloseTabNotification) { 326 lazy.CloseRemoteTab.closeTabNotificationCount = 0; 327 } 328 lazy.CloseRemoteTab.closeTabNotificationCount += totalClosedTabs; 329 const [title, body] = await lazy.accountsL10n.formatValues([ 330 { 331 id: "account-tabs-closed-remotely", 332 args: { closedCount: lazy.CloseRemoteTab.closeTabNotificationCount }, 333 }, 334 { id: "account-view-recently-closed-tabs" }, 335 ]); 336 337 try { 338 let alert = new AlertNotification({ 339 title, 340 text: body, 341 textClickable: true, 342 name: "closed-tab-notification", 343 }); 344 lazy.AlertsService.showAlert(alert, clickCallback); 345 } catch (ex) { 346 console.error("Error notifying user of closed tab(s) ", ex); 347 } 348 }, 349 350 async _onVerifyLoginNotification({ body, title, url }) { 351 let tab; 352 let win = lazy.BrowserWindowTracker.getTopWindow({ private: false }); 353 if (!win) { 354 win = await this._openURLInNewWindow(url); 355 let tabs = win.gBrowser.tabs; 356 tab = tabs[tabs.length - 1]; 357 } else { 358 tab = win.gBrowser.addWebTab(url); 359 } 360 tab.attention = true; 361 let clickCallback = (subject, topic) => { 362 if (topic != "alertclickcallback") { 363 return; 364 } 365 win.gBrowser.selectedTab = tab; 366 }; 367 368 try { 369 let alert = new AlertNotification({ 370 title, 371 body, 372 textClickable: true, 373 }); 374 lazy.AlertsService.showAlert(alert, clickCallback); 375 } catch (ex) { 376 console.error("Error notifying of a verify login event: ", ex); 377 } 378 }, 379 380 _onDeviceConnected(deviceName) { 381 const [title, body] = lazy.accountsL10n.formatValuesSync([ 382 { id: "account-connection-title-2" }, 383 deviceName 384 ? { id: "account-connection-connected-with", args: { deviceName } } 385 : { id: "account-connection-connected-with-noname" }, 386 ]); 387 388 let clickCallback = async (subject, topic) => { 389 if (topic != "alertclickcallback") { 390 return; 391 } 392 let url = await lazy.FxAccounts.config.promiseManageDevicesURI( 393 "device-connected-notification" 394 ); 395 let win = lazy.BrowserWindowTracker.getTopWindow({ private: false }); 396 if (!win) { 397 this._openURLInNewWindow(url); 398 } else { 399 win.gBrowser.addWebTab(url); 400 } 401 }; 402 403 try { 404 let alert = new AlertNotification({ 405 title, 406 text: body, 407 textClickable: true, 408 }); 409 lazy.AlertsService.showAlert(alert, clickCallback); 410 } catch (ex) { 411 console.error("Error notifying of a new Sync device: ", ex); 412 } 413 }, 414 415 _onDeviceDisconnected() { 416 const [title, body] = lazy.accountsL10n.formatValuesSync([ 417 "account-connection-title-2", 418 "account-connection-disconnected", 419 ]); 420 421 let clickCallback = (subject, topic) => { 422 if (topic != "alertclickcallback") { 423 return; 424 } 425 this._openPreferences("sync"); 426 }; 427 428 let alert = new AlertNotification({ 429 title, 430 text: body, 431 textClickable: true, 432 }); 433 lazy.AlertsService.showAlert(alert, clickCallback); 434 }, 435 436 _updateFxaBadges(win) { 437 let fxaButton = win.document.getElementById("fxa-toolbar-menu-button"); 438 let badge = fxaButton?.querySelector(".toolbarbutton-badge"); 439 440 let state = lazy.UIState.get(); 441 if ( 442 state.status == lazy.UIState.STATUS_LOGIN_FAILED || 443 state.status == lazy.UIState.STATUS_NOT_VERIFIED 444 ) { 445 // If the fxa toolbar button is in the toolbox, we display the notification 446 // on the fxa button instead of the app menu. 447 let navToolbox = win.document.getElementById("navigator-toolbox"); 448 let isFxAButtonShown = navToolbox.contains(fxaButton); 449 if (isFxAButtonShown) { 450 state.status == lazy.UIState.STATUS_LOGIN_FAILED 451 ? fxaButton?.setAttribute("badge-status", state.status) 452 : badge?.classList.add("feature-callout"); 453 } else { 454 lazy.AppMenuNotifications.showBadgeOnlyNotification( 455 "fxa-needs-authentication" 456 ); 457 } 458 } else { 459 fxaButton?.removeAttribute("badge-status"); 460 badge?.classList.remove("feature-callout"); 461 lazy.AppMenuNotifications.removeNotification("fxa-needs-authentication"); 462 } 463 }, 464 465 // Open preferences even if there are no open windows. 466 async _openPreferences(...args) { 467 let chromeWindow = lazy.BrowserWindowTracker.getTopWindow(); 468 if (!chromeWindow && AppConstants.platform !== "macosx") { 469 // If we're not on macOS, there may be no windows open in this 470 // workspace, so open a new one. (the macOS case is handled below) 471 // 472 // This should get cleaned up in bug 1983081 since openPreferences() 473 // shouldn't require a window argument. 474 chromeWindow = await lazy.BrowserWindowTracker.promiseOpenWindow(); 475 } 476 if (chromeWindow) { 477 chromeWindow.openPreferences(...args); 478 return; 479 } 480 481 if (AppConstants.platform == "macosx") { 482 Services.appShell.hiddenDOMWindow.openPreferences(...args); 483 } 484 }, 485 };