sync.js (33016B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* import-globals-from preferences.js */ 6 7 const { SCOPE_APP_SYNC } = ChromeUtils.importESModule( 8 "resource://gre/modules/FxAccountsCommon.sys.mjs" 9 ); 10 11 const FXA_PAGE_LOGGED_OUT = 0; 12 const FXA_PAGE_LOGGED_IN = 1; 13 14 // Indexes into the "login status" deck. 15 // We are in a successful verified state - everything should work! 16 const FXA_LOGIN_VERIFIED = 0; 17 // We have logged in to an unverified account. 18 const FXA_LOGIN_UNVERIFIED = 1; 19 // We are logged in locally, but the server rejected our credentials. 20 const FXA_LOGIN_FAILED = 2; 21 22 // Indexes into the "sync status" deck. 23 const SYNC_DISCONNECTED = 0; 24 const SYNC_CONNECTED = 1; 25 26 const BACKUP_ARCHIVE_ENABLED_PREF_NAME = "browser.backup.archive.enabled"; 27 const BACKUP_RESTORE_ENABLED_PREF_NAME = "browser.backup.restore.enabled"; 28 29 ChromeUtils.defineESModuleGetters(lazy, { 30 BackupService: "resource:///modules/backup/BackupService.sys.mjs", 31 }); 32 33 Preferences.addAll([ 34 // sync 35 { id: "services.sync.engine.bookmarks", type: "bool" }, 36 { id: "services.sync.engine.history", type: "bool" }, 37 { id: "services.sync.engine.tabs", type: "bool" }, 38 { id: "services.sync.engine.passwords", type: "bool" }, 39 { id: "services.sync.engine.addresses", type: "bool" }, 40 { id: "services.sync.engine.creditcards", type: "bool" }, 41 { id: "services.sync.engine.addons", type: "bool" }, 42 { id: "services.sync.engine.prefs", type: "bool" }, 43 ]); 44 45 /** 46 * A helper class for managing sync related UI behavior. 47 */ 48 var SyncHelpers = new (class SyncHelpers { 49 /** 50 * href for Connect another device link. 51 * 52 * @type {string} 53 */ 54 connectAnotherDeviceHref = ""; 55 56 /** 57 * Returns the current global UIState. 58 * 59 * @type {object} 60 * @readonly 61 */ 62 get uiState() { 63 let state = UIState.get(); 64 return state; 65 } 66 67 /** 68 * Retrieves the current UI state status from the global UIState. 69 * 70 * @type {string} 71 * @readonly 72 */ 73 get uiStateStatus() { 74 return this.uiState.status; 75 } 76 77 /** 78 * Whether Sync is currently enabled in the UIState. 79 * 80 * @type {boolean} 81 * @readonly 82 */ 83 get isSyncEnabled() { 84 return this.uiState.syncEnabled; 85 } 86 87 /** 88 * Extracts and sanitizes the `entrypoint` parameter from the current document URL. 89 * 90 * @returns {string} The sanitized entry point name. 91 */ 92 getEntryPoint() { 93 let params = URL.fromURI(document.documentURIObject).searchParams; 94 let entryPoint = params.get("entrypoint") || "preferences"; 95 entryPoint = entryPoint.replace(/[^-.\w]/g, ""); 96 return entryPoint; 97 } 98 99 /** 100 * Replace the current tab with the specified URL. 101 * 102 * @param {string} url 103 */ 104 replaceTabWithUrl(url) { 105 // Get the <browser> element hosting us. 106 let browser = window.docShell.chromeEventHandler; 107 // And tell it to load our URL. 108 browser.loadURI(Services.io.newURI(url), { 109 triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( 110 {} 111 ), 112 }); 113 } 114 115 /** 116 * Opens the "Choose What to Sync" dialog and handles user interaction. 117 * 118 * @param {boolean} isSyncConfigured 119 * Whether Sync is already configured for this profile. 120 * @param {string|null} [why=null] 121 * Optional reason or event name indicating why the dialog was opened. 122 * @returns {Promise<void>} 123 * Resolves when the dialog flow and any post-actions have completed. 124 */ 125 async _chooseWhatToSync(isSyncConfigured, why = null) { 126 // Record the user opening the choose what to sync menu. 127 fxAccounts.telemetry.recordOpenCWTSMenu(why).catch(err => { 128 console.error("Failed to record open CWTS menu event", err); 129 }); 130 131 // Assuming another device is syncing and we're not, 132 // we update the engines selection so the correct 133 // checkboxes are pre-filed. 134 if (!isSyncConfigured) { 135 try { 136 await Weave.Service.updateLocalEnginesState(); 137 } catch (err) { 138 console.error("Error updating the local engines state", err); 139 } 140 } 141 let params = {}; 142 if (isSyncConfigured) { 143 // If we are already syncing then we also offer to disconnect. 144 params.disconnectFun = () => this.disconnectSync(); 145 } 146 gSubDialog.open( 147 "chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml", 148 { 149 closingCallback: event => { 150 if (event.detail.button == "accept") { 151 // Sync wasn't previously configured, but the user has accepted 152 // so we want to now start syncing! 153 if (!isSyncConfigured) { 154 fxAccounts.telemetry 155 .recordConnection(["sync"], "ui") 156 .then(() => { 157 return Weave.Service.configure(); 158 }) 159 .catch(err => { 160 console.error("Failed to enable sync", err); 161 }); 162 } else { 163 // User is already configured and have possibly changed the engines they want to 164 // sync, so we should let the server know immediately 165 // if the user is currently syncing, we queue another sync after 166 // to ensure we caught their updates 167 Services.tm.dispatchToMainThread(() => { 168 Weave.Service.queueSync("cwts"); 169 }); 170 } 171 } 172 // When the modal closes we want to remove any query params 173 // so it doesn't open on subsequent visits (and will reload) 174 const browser = window.docShell.chromeEventHandler; 175 browser.loadURI(Services.io.newURI("about:preferences#sync"), { 176 triggeringPrincipal: 177 Services.scriptSecurityManager.getSystemPrincipal(), 178 }); 179 }, 180 }, 181 params /* aParams */ 182 ); 183 } 184 185 // Disconnect sync, leaving the account connected. 186 disconnectSync() { 187 return window.browsingContext.topChromeWindow.gSync.disconnect({ 188 confirm: true, 189 disconnectAccount: false, 190 }); 191 } 192 193 async setupSync() { 194 try { 195 const hasKeys = await fxAccounts.keys.hasKeysForScope(SCOPE_APP_SYNC); 196 if (hasKeys) { 197 // User has keys - open the choose what to sync dialog 198 this._chooseWhatToSync(false, "setupSync"); 199 } else { 200 // User signed in via third-party auth without sync keys. 201 // Redirect to FxA to create a password and generate sync keys. 202 // canConnectAccount() checks if the Primary Password is locked and 203 // prompts the user to unlock it. Returns false if the user cancels. 204 if (!(await FxAccounts.canConnectAccount())) { 205 return; 206 } 207 const url = await FxAccounts.config.promiseConnectAccountURI( 208 this.getEntryPoint() 209 ); 210 this.replaceTabWithUrl(url); 211 } 212 } catch (err) { 213 console.error("Failed to check for sync keys", err); 214 // Fallback to opening CWTS dialog 215 this._chooseWhatToSync(false, "setupSync"); 216 } 217 } 218 219 async signIn() { 220 if (!(await FxAccounts.canConnectAccount())) { 221 return; 222 } 223 const url = await FxAccounts.config.promiseConnectAccountURI( 224 this.getEntryPoint() 225 ); 226 this.replaceTabWithUrl(url); 227 } 228 229 /** 230 * Attempts to take the user through the sign in flow by opening the web content 231 * with the given entrypoint as a query parameter 232 * 233 * @param {string} entrypoint 234 * An string appended to the query parameters, used in telemetry to differentiate 235 * different entrypoints to accounts 236 */ 237 async reSignIn(entrypoint) { 238 const url = await FxAccounts.config.promiseConnectAccountURI(entrypoint); 239 this.replaceTabWithUrl(url); 240 } 241 242 async verifyFirefoxAccount() { 243 return this.reSignIn("preferences-reverify"); 244 } 245 246 /** 247 * Disconnect the account, including everything linked. 248 * 249 * @param {boolean} confirm 250 * Whether to show a confirmation dialog before disconnecting 251 */ 252 unlinkFirefoxAccount(confirm) { 253 window.browsingContext.topChromeWindow.gSync.disconnect({ 254 confirm, 255 }); 256 } 257 })(); 258 259 Preferences.addSetting({ 260 id: "uiStateUpdate", 261 setup(emitChange) { 262 Weave.Svc.Obs.add(UIState.ON_UPDATE, emitChange); 263 return () => Weave.Svc.Obs.remove(UIState.ON_UPDATE, emitChange); 264 }, 265 }); 266 267 // Mozilla accounts section 268 269 // Logged out of Mozilla account 270 Preferences.addSetting({ 271 id: "noFxaAccountGroup", 272 deps: ["uiStateUpdate"], 273 visible() { 274 return SyncHelpers.uiStateStatus == UIState.STATUS_NOT_CONFIGURED; 275 }, 276 }); 277 Preferences.addSetting({ 278 id: "noFxaAccount", 279 }); 280 Preferences.addSetting({ 281 id: "noFxaSignIn", 282 onUserClick: () => { 283 SyncHelpers.signIn(); 284 }, 285 }); 286 287 // Logged in and verified and all is good 288 Preferences.addSetting({ 289 id: "fxaSignedInGroup", 290 deps: ["uiStateUpdate"], 291 visible() { 292 return SyncHelpers.uiStateStatus == UIState.STATUS_SIGNED_IN; 293 }, 294 }); 295 Preferences.addSetting({ 296 id: "fxaLoginVerified", 297 deps: ["uiStateUpdate"], 298 _failedAvatarURLs: new Set(), 299 getControlConfig(config, _, setting) { 300 let state = SyncHelpers.uiState; 301 302 if (state.displayName) { 303 config.l10nId = "sync-account-signed-in-display-name"; 304 config.l10nArgs = { 305 name: state.displayName, 306 email: state.email || "", 307 }; 308 } else { 309 config.l10nId = "sync-account-signed-in"; 310 config.l10nArgs = { 311 email: state.email || "", 312 }; 313 } 314 315 // Reset the image to default avatar if we encounter an error. 316 if (this._failedAvatarURLs.has(state.avatarURL)) { 317 config.iconSrc = "chrome://browser/skin/fxa/avatar-color.svg"; 318 return config; 319 } 320 321 if (state.avatarURL && !state.avatarIsDefault) { 322 config.iconSrc = state.avatarURL; 323 let img = new Image(); 324 img.onerror = () => { 325 this._failedAvatarURLs.add(state.avatarURL); 326 setting.onChange(); 327 }; 328 img.src = state.avatarURL; 329 } 330 return config; 331 }, 332 }); 333 Preferences.addSetting( 334 class extends Preferences.AsyncSetting { 335 static id = "verifiedManage"; 336 337 setup() { 338 Weave.Svc.Obs.add(UIState.ON_UPDATE, this.emitChange); 339 return () => Weave.Svc.Obs.remove(UIState.ON_UPDATE, this.emitChange); 340 } 341 342 // The "manage account" link embeds the uid, so we need to update this 343 // if the account state changes. 344 async getControlConfig() { 345 let href = await FxAccounts.config.promiseManageURI( 346 SyncHelpers.getEntryPoint() 347 ); 348 return { 349 controlAttrs: { 350 href: href ?? "https://accounts.firefox.com/settings", 351 }, 352 }; 353 } 354 } 355 ); 356 357 Preferences.addSetting({ 358 id: "fxaUnlinkButton", 359 onUserClick: () => { 360 SyncHelpers.unlinkFirefoxAccount(true); 361 }, 362 }); 363 364 // Logged in to an unverified account 365 Preferences.addSetting({ 366 id: "fxaUnverifiedGroup", 367 deps: ["uiStateUpdate"], 368 visible() { 369 return SyncHelpers.uiStateStatus == UIState.STATUS_NOT_VERIFIED; 370 }, 371 }); 372 Preferences.addSetting({ 373 id: "fxaLoginUnverified", 374 deps: ["uiStateUpdate"], 375 getControlConfig(config) { 376 let state = SyncHelpers.uiState; 377 config.l10nArgs = { 378 email: state.email || "", 379 }; 380 return config; 381 }, 382 }); 383 Preferences.addSetting({ 384 id: "verifyFxaAccount", 385 onUserClick: () => { 386 SyncHelpers.verifyFirefoxAccount(); 387 }, 388 }); 389 Preferences.addSetting({ 390 id: "unverifiedUnlinkFxaAccount", 391 onUserClick: () => { 392 /* no warning as account can't have previously synced */ 393 SyncHelpers.unlinkFirefoxAccount(false); 394 }, 395 }); 396 397 // Logged in locally but server rejected credentials 398 Preferences.addSetting({ 399 id: "fxaLoginRejectedGroup", 400 deps: ["uiStateUpdate"], 401 visible() { 402 return SyncHelpers.uiStateStatus == UIState.STATUS_LOGIN_FAILED; 403 }, 404 }); 405 Preferences.addSetting({ 406 id: "fxaLoginRejected", 407 deps: ["uiStateUpdate"], 408 getControlConfig(config) { 409 let state = SyncHelpers.uiState; 410 config.l10nArgs = { 411 email: state.email || "", 412 }; 413 return config; 414 }, 415 }); 416 Preferences.addSetting({ 417 id: "rejectReSignIn", 418 onUserClick: () => { 419 SyncHelpers.reSignIn(SyncHelpers.getEntryPoint()); 420 }, 421 }); 422 Preferences.addSetting({ 423 id: "rejectUnlinkFxaAccount", 424 onUserClick: () => { 425 SyncHelpers.unlinkFirefoxAccount(true); 426 }, 427 }); 428 429 //Sync section 430 431 //Sync section - no Firefox account 432 Preferences.addSetting({ 433 id: "syncNoFxaSignIn", 434 deps: ["uiStateUpdate"], 435 visible() { 436 return SyncHelpers.uiStateStatus === UIState.STATUS_NOT_CONFIGURED; 437 }, 438 onUserClick: () => { 439 SyncHelpers.signIn(); 440 }, 441 }); 442 443 // Sync section - Syncing is OFF 444 Preferences.addSetting({ 445 id: "syncNotConfigured", 446 deps: ["uiStateUpdate"], 447 visible() { 448 return ( 449 SyncHelpers.uiStateStatus === UIState.STATUS_SIGNED_IN && 450 !SyncHelpers.isSyncEnabled 451 ); 452 }, 453 }); 454 Preferences.addSetting({ 455 id: "syncSetup", 456 onUserClick: () => SyncHelpers.setupSync(), 457 }); 458 459 // Sync section - Syncing is ON 460 Preferences.addSetting({ 461 id: "syncConfigured", 462 deps: ["uiStateUpdate"], 463 visible() { 464 return ( 465 SyncHelpers.uiStateStatus === UIState.STATUS_SIGNED_IN && 466 SyncHelpers.isSyncEnabled 467 ); 468 }, 469 }); 470 471 Preferences.addSetting({ 472 id: "syncStatus", 473 }); 474 Preferences.addSetting({ 475 id: "syncNow", 476 deps: ["uiStateUpdate"], 477 onUserClick() { 478 Weave.Service.sync({ why: "aboutprefs" }); 479 }, 480 visible: () => !SyncHelpers.uiState.syncing, 481 // Bug 2004864 - add tooltip 482 }); 483 Preferences.addSetting({ 484 id: "syncing", 485 deps: ["uiStateUpdate"], 486 disabled: () => SyncHelpers.uiState.syncing, 487 visible: () => SyncHelpers.uiState.syncing, 488 }); 489 490 const SYNC_ENGINE_SETTINGS = [ 491 { 492 id: "syncBookmarks", 493 pref: "services.sync.engine.bookmarks", 494 type: "bookmarks", 495 }, 496 { id: "syncHistory", pref: "services.sync.engine.history", type: "history" }, 497 { id: "syncTabs", pref: "services.sync.engine.tabs", type: "tabs" }, 498 { 499 id: "syncPasswords", 500 pref: "services.sync.engine.passwords", 501 type: "passwords", 502 }, 503 { 504 id: "syncAddresses", 505 pref: "services.sync.engine.addresses", 506 type: "addresses", 507 }, 508 { 509 id: "syncPayments", 510 pref: "services.sync.engine.creditcards", 511 type: "payments", 512 }, 513 { id: "syncAddons", pref: "services.sync.engine.addons", type: "addons" }, 514 { id: "syncSettings", pref: "services.sync.engine.prefs", type: "settings" }, 515 ]; 516 517 SYNC_ENGINE_SETTINGS.forEach(({ id, pref }) => { 518 Preferences.addSetting({ id, pref }); 519 }); 520 521 Preferences.addSetting({ 522 id: "syncEnginesList", 523 deps: SYNC_ENGINE_SETTINGS.map(({ id }) => id), 524 getControlConfig(config, deps) { 525 const engines = SYNC_ENGINE_SETTINGS.filter( 526 ({ id }) => deps[id]?.value 527 ).map(({ type }) => type); 528 529 return { 530 ...config, 531 controlAttrs: { 532 ...config.controlAttrs, 533 ".engines": engines, 534 }, 535 }; 536 }, 537 }); 538 539 Preferences.addSetting({ 540 id: "syncChangeOptions", 541 onUserClick: () => { 542 SyncHelpers._chooseWhatToSync(true, "manageSyncSettings"); 543 }, 544 }); 545 546 // Sync section - Device name 547 Preferences.addSetting({ 548 id: "fxaDeviceNameSection", 549 deps: ["uiStateUpdate"], 550 visible() { 551 return SyncHelpers.uiStateStatus !== UIState.STATUS_NOT_CONFIGURED; 552 }, 553 }); 554 Preferences.addSetting({ 555 id: "fxaDeviceNameGroup", 556 }); 557 Preferences.addSetting({ 558 id: "fxaDeviceName", 559 deps: ["uiStateUpdate"], 560 get: () => Weave.Service.clientsEngine.localName, 561 set(val) { 562 Weave.Service.clientsEngine.localName = val; 563 }, 564 disabled() { 565 return SyncHelpers.uiStateStatus !== UIState.STATUS_SIGNED_IN; 566 }, 567 getControlConfig(config) { 568 if (config.controlAttrs?.defaultvalue) { 569 return config; 570 } 571 const deviceDefaultLocalName = fxAccounts?.device?.getDefaultLocalName(); 572 if (deviceDefaultLocalName) { 573 return { 574 ...config, 575 controlAttrs: { 576 ...config.controlAttrs, 577 defaultvalue: deviceDefaultLocalName, 578 }, 579 }; 580 } 581 return config; 582 }, 583 }); 584 Preferences.addSetting({ 585 id: "fxaConnectAnotherDevice", 586 getControlConfig(config) { 587 if (SyncHelpers.connectAnotherDeviceHref) { 588 return { 589 ...config, 590 controlAttrs: { 591 ...config.controlAttrs, 592 href: SyncHelpers.connectAnotherDeviceHref, 593 }, 594 }; 595 } 596 return config; 597 }, 598 setup(emitChange) { 599 FxAccounts.config 600 .promiseConnectDeviceURI(SyncHelpers.getEntryPoint()) 601 .then(connectURI => { 602 SyncHelpers.connectAnotherDeviceHref = connectURI; 603 emitChange(); 604 }); 605 }, 606 }); 607 608 var gSyncPane = { 609 get page() { 610 return document.getElementById("weavePrefsDeck").selectedIndex; 611 }, 612 613 set page(val) { 614 document.getElementById("weavePrefsDeck").selectedIndex = val; 615 }, 616 617 init() { 618 this._setupEventListeners(); 619 this.setupEnginesUI(); 620 this.updateSyncUI(); 621 622 document 623 .getElementById("weavePrefsDeck") 624 .removeAttribute("data-hidden-from-search"); 625 626 // If the Service hasn't finished initializing, wait for it. 627 let xps = Cc["@mozilla.org/weave/service;1"].getService( 628 Ci.nsISupports 629 ).wrappedJSObject; 630 631 if (xps.ready) { 632 this._init(); 633 return; 634 } 635 636 // it may take some time before all the promises we care about resolve, so 637 // pre-load what we can from synchronous sources. 638 this._showLoadPage(xps); 639 640 let onUnload = function () { 641 window.removeEventListener("unload", onUnload); 642 try { 643 Services.obs.removeObserver(onReady, "weave:service:ready"); 644 } catch (e) {} 645 }; 646 647 let onReady = () => { 648 Services.obs.removeObserver(onReady, "weave:service:ready"); 649 window.removeEventListener("unload", onUnload); 650 this._init(); 651 }; 652 653 Services.obs.addObserver(onReady, "weave:service:ready"); 654 window.addEventListener("unload", onUnload); 655 656 xps.ensureLoaded(); 657 }, 658 659 /** 660 * This method allows us to override any hidden states that were set 661 * during preferences.js init(). Currently, this is used to hide the 662 * backup section if backup is disabled. 663 * 664 * Take caution when trying to flip the hidden state to true since the 665 * element might show up unexpectedly on different pages in about:preferences 666 * since this function will run at the end of preferences.js init(). 667 * 668 * See Bug 1999032 to remove this in favor of config-based prefs. 669 */ 670 handlePrefControlledSection() { 671 let bs = lazy.BackupService.init(); 672 673 if (!bs.archiveEnabledStatus.enabled && !bs.restoreEnabledStatus.enabled) { 674 document.getElementById("backupCategory").hidden = true; 675 document.getElementById("dataBackupGroup").hidden = true; 676 } 677 }, 678 679 _showLoadPage() { 680 let maybeAcct = false; 681 let username = Services.prefs.getCharPref("services.sync.username", ""); 682 if (username) { 683 document.getElementById("fxaEmailAddress").textContent = username; 684 maybeAcct = true; 685 } 686 687 let cachedComputerName = Services.prefs.getStringPref( 688 "identity.fxaccounts.account.device.name", 689 "" 690 ); 691 if (cachedComputerName) { 692 maybeAcct = true; 693 this._populateComputerName(cachedComputerName); 694 } 695 this.page = maybeAcct ? FXA_PAGE_LOGGED_IN : FXA_PAGE_LOGGED_OUT; 696 }, 697 698 _init() { 699 initSettingGroup("sync"); 700 initSettingGroup("account"); 701 702 Weave.Svc.Obs.add(UIState.ON_UPDATE, this.updateWeavePrefs, this); 703 704 window.addEventListener("unload", () => { 705 Weave.Svc.Obs.remove(UIState.ON_UPDATE, this.updateWeavePrefs, this); 706 }); 707 708 FxAccounts.config 709 .promiseConnectDeviceURI(SyncHelpers.getEntryPoint()) 710 .then(connectURI => { 711 document 712 .getElementById("connect-another-device") 713 .setAttribute("href", connectURI); 714 }); 715 716 // Links for mobile devices. 717 for (let platform of ["android", "ios"]) { 718 let url = 719 Services.prefs.getCharPref(`identity.mobilepromo.${platform}`) + 720 "sync-preferences"; 721 for (let elt of document.querySelectorAll( 722 `.fxaMobilePromo-${platform}` 723 )) { 724 elt.setAttribute("href", url); 725 } 726 } 727 728 this.updateWeavePrefs(); 729 730 // Notify observers that the UI is now ready 731 Services.obs.notifyObservers(window, "sync-pane-loaded"); 732 733 this._maybeShowSyncAction(); 734 }, 735 736 // Check if the user is coming from a call to action 737 // and show them the correct additional panel 738 _maybeShowSyncAction() { 739 if ( 740 location.hash == "#sync" && 741 UIState.get().status == UIState.STATUS_SIGNED_IN 742 ) { 743 if (location.href.includes("action=pair")) { 744 gSyncPane.pairAnotherDevice(); 745 } else if (location.href.includes("action=choose-what-to-sync")) { 746 SyncHelpers._chooseWhatToSync(false, "callToAction"); 747 } 748 } 749 }, 750 751 _toggleComputerNameControls(editMode) { 752 let textbox = document.getElementById("fxaSyncComputerName"); 753 textbox.disabled = !editMode; 754 document.getElementById("fxaChangeDeviceName").hidden = editMode; 755 document.getElementById("fxaCancelChangeDeviceName").hidden = !editMode; 756 document.getElementById("fxaSaveChangeDeviceName").hidden = !editMode; 757 }, 758 759 _focusComputerNameTextbox() { 760 let textbox = document.getElementById("fxaSyncComputerName"); 761 let valLength = textbox.value.length; 762 textbox.focus(); 763 textbox.setSelectionRange(valLength, valLength); 764 }, 765 766 _blurComputerNameTextbox() { 767 document.getElementById("fxaSyncComputerName").blur(); 768 }, 769 770 _focusAfterComputerNameTextbox() { 771 // Focus the most appropriate element that's *not* the "computer name" box. 772 Services.focus.moveFocus( 773 window, 774 document.getElementById("fxaSyncComputerName"), 775 Services.focus.MOVEFOCUS_FORWARD, 776 0 777 ); 778 }, 779 780 _updateComputerNameValue(save) { 781 if (save) { 782 let textbox = document.getElementById("fxaSyncComputerName"); 783 Weave.Service.clientsEngine.localName = textbox.value; 784 } 785 this._populateComputerName(Weave.Service.clientsEngine.localName); 786 }, 787 788 _setupEventListeners() { 789 function setEventListener(aId, aEventType, aCallback) { 790 document 791 .getElementById(aId) 792 .addEventListener(aEventType, aCallback.bind(gSyncPane)); 793 } 794 795 setEventListener("openChangeProfileImage", "click", function (event) { 796 gSyncPane.openChangeProfileImage(event); 797 }); 798 setEventListener("openChangeProfileImage", "keypress", function (event) { 799 gSyncPane.openChangeProfileImage(event); 800 }); 801 setEventListener("fxaChangeDeviceName", "command", function () { 802 this._toggleComputerNameControls(true); 803 this._focusComputerNameTextbox(); 804 }); 805 setEventListener("fxaCancelChangeDeviceName", "command", function () { 806 // We explicitly blur the textbox because of bug 75324, then after 807 // changing the state of the buttons, force focus to whatever the focus 808 // manager thinks should be next (which on the mac, depends on an OSX 809 // keyboard access preference) 810 this._blurComputerNameTextbox(); 811 this._toggleComputerNameControls(false); 812 this._updateComputerNameValue(false); 813 this._focusAfterComputerNameTextbox(); 814 }); 815 setEventListener("fxaSaveChangeDeviceName", "command", function () { 816 // Work around bug 75324 - see above. 817 this._blurComputerNameTextbox(); 818 this._toggleComputerNameControls(false); 819 this._updateComputerNameValue(true); 820 this._focusAfterComputerNameTextbox(); 821 }); 822 setEventListener("noFxaSignIn", "command", function () { 823 SyncHelpers.signIn(); 824 return false; 825 }); 826 setEventListener("fxaUnlinkButton", "command", function () { 827 SyncHelpers.unlinkFirefoxAccount(true); 828 }); 829 setEventListener("verifyFxaAccount", "command", () => 830 SyncHelpers.verifyFirefoxAccount() 831 ); 832 setEventListener("unverifiedUnlinkFxaAccount", "command", function () { 833 /* no warning as account can't have previously synced */ 834 SyncHelpers.unlinkFirefoxAccount(false); 835 }); 836 setEventListener("rejectReSignIn", "command", function () { 837 SyncHelpers.reSignIn(SyncHelpers.getEntryPoint()); 838 }); 839 setEventListener("rejectUnlinkFxaAccount", "command", function () { 840 SyncHelpers.unlinkFirefoxAccount(true); 841 }); 842 setEventListener("fxaSyncComputerName", "keypress", function (e) { 843 if (e.keyCode == KeyEvent.DOM_VK_RETURN) { 844 document.getElementById("fxaSaveChangeDeviceName").click(); 845 } else if (e.keyCode == KeyEvent.DOM_VK_ESCAPE) { 846 document.getElementById("fxaCancelChangeDeviceName").click(); 847 } 848 }); 849 setEventListener("syncSetup", "command", () => SyncHelpers.setupSync()); 850 setEventListener("syncChangeOptions", "command", function () { 851 SyncHelpers._chooseWhatToSync(true, "manageSyncSettings"); 852 }); 853 setEventListener("syncNow", "command", function () { 854 // syncing can take a little time to send the "started" notification, so 855 // pretend we already got it. 856 this._updateSyncNow(true); 857 Weave.Service.sync({ why: "aboutprefs" }); 858 }); 859 setEventListener("syncNow", "mouseover", function () { 860 const state = UIState.get(); 861 // If we are currently syncing, just set the tooltip to the same as the 862 // button label (ie, "Syncing...") 863 let tooltiptext = state.syncing 864 ? document.getElementById("syncNow").getAttribute("label") 865 : window.browsingContext.topChromeWindow.gSync.formatLastSyncDate( 866 state.lastSync 867 ); 868 document 869 .getElementById("syncNow") 870 .setAttribute("tooltiptext", tooltiptext); 871 }); 872 }, 873 874 updateSyncUI() { 875 let syncStatusTitle = document.getElementById("syncStatusTitle"); 876 let syncNowButton = document.getElementById("syncNow"); 877 let syncNotConfiguredEl = document.getElementById("syncNotConfigured"); 878 let syncConfiguredEl = document.getElementById("syncConfigured"); 879 880 if (SyncHelpers.isSyncEnabled) { 881 syncStatusTitle.setAttribute("data-l10n-id", "prefs-syncing-on"); 882 syncNowButton.hidden = false; 883 syncConfiguredEl.hidden = false; 884 syncNotConfiguredEl.hidden = true; 885 } else { 886 syncStatusTitle.setAttribute("data-l10n-id", "prefs-syncing-off"); 887 syncNowButton.hidden = true; 888 syncConfiguredEl.hidden = true; 889 syncNotConfiguredEl.hidden = false; 890 } 891 }, 892 893 _updateSyncNow(syncing) { 894 let butSyncNow = document.getElementById("syncNow"); 895 let fluentID = syncing ? "prefs-syncing-button" : "prefs-sync-now-button"; 896 if (document.l10n.getAttributes(butSyncNow).id != fluentID) { 897 // Only one of the two strings has an accesskey, and fluent won't 898 // remove it if we switch to the string that doesn't, so just force 899 // removal here. 900 butSyncNow.removeAttribute("accesskey"); 901 document.l10n.setAttributes(butSyncNow, fluentID); 902 } 903 butSyncNow.disabled = syncing; 904 }, 905 906 updateWeavePrefs() { 907 let service = Cc["@mozilla.org/weave/service;1"].getService( 908 Ci.nsISupports 909 ).wrappedJSObject; 910 911 let displayNameLabel = document.getElementById("fxaDisplayName"); 912 let fxaEmailAddressLabels = document.querySelectorAll( 913 ".l10nArgsEmailAddress" 914 ); 915 displayNameLabel.hidden = true; 916 917 // while we determine the fxa status pre-load what we can. 918 this._showLoadPage(service); 919 920 let state = UIState.get(); 921 if (state.status == UIState.STATUS_NOT_CONFIGURED) { 922 this.page = FXA_PAGE_LOGGED_OUT; 923 return; 924 } 925 this.page = FXA_PAGE_LOGGED_IN; 926 // We are logged in locally, but maybe we are in a state where the 927 // server rejected our credentials (eg, password changed on the server) 928 let fxaLoginStatus = document.getElementById("fxaLoginStatus"); 929 let syncReady = false; // Is sync able to actually sync? 930 // We need to check error states that need a re-authenticate to resolve 931 // themselves first. 932 if (state.status == UIState.STATUS_LOGIN_FAILED) { 933 fxaLoginStatus.selectedIndex = FXA_LOGIN_FAILED; 934 } else if (state.status == UIState.STATUS_NOT_VERIFIED) { 935 fxaLoginStatus.selectedIndex = FXA_LOGIN_UNVERIFIED; 936 } else { 937 // We must be golden (or in an error state we expect to magically 938 // resolve itself) 939 fxaLoginStatus.selectedIndex = FXA_LOGIN_VERIFIED; 940 syncReady = true; 941 } 942 fxaEmailAddressLabels.forEach(label => { 943 let l10nAttrs = document.l10n.getAttributes(label); 944 document.l10n.setAttributes(label, l10nAttrs.id, { email: state.email }); 945 }); 946 document.getElementById("fxaEmailAddress").textContent = state.email; 947 948 this._populateComputerName(Weave.Service.clientsEngine.localName); 949 for (let elt of document.querySelectorAll(".needs-account-ready")) { 950 elt.disabled = !syncReady; 951 } 952 953 // Clear the profile image (if any) of the previously logged in account. 954 document 955 .querySelector("#fxaLoginVerified > .fxaProfileImage") 956 .style.removeProperty("list-style-image"); 957 958 if (state.displayName) { 959 fxaLoginStatus.setAttribute("hasName", true); 960 displayNameLabel.hidden = false; 961 document.getElementById("fxaDisplayNameHeading").textContent = 962 state.displayName; 963 } else { 964 fxaLoginStatus.removeAttribute("hasName"); 965 } 966 if (state.avatarURL && !state.avatarIsDefault) { 967 let bgImage = 'url("' + state.avatarURL + '")'; 968 let profileImageElement = document.querySelector( 969 "#fxaLoginVerified > .fxaProfileImage" 970 ); 971 profileImageElement.style.listStyleImage = bgImage; 972 973 let img = new Image(); 974 img.onerror = () => { 975 // Clear the image if it has trouble loading. Since this callback is asynchronous 976 // we check to make sure the image is still the same before we clear it. 977 if (profileImageElement.style.listStyleImage === bgImage) { 978 profileImageElement.style.removeProperty("list-style-image"); 979 } 980 }; 981 img.src = state.avatarURL; 982 } 983 // The "manage account" link embeds the uid, so we need to update this 984 // if the account state changes. 985 FxAccounts.config 986 .promiseManageURI(SyncHelpers.getEntryPoint()) 987 .then(accountsManageURI => { 988 document 989 .getElementById("verifiedManage") 990 .setAttribute("href", accountsManageURI); 991 }); 992 // and the actual sync state. 993 let eltSyncStatus = document.getElementById("syncStatusContainer"); 994 eltSyncStatus.hidden = !syncReady; 995 this._updateSyncNow(state.syncing); 996 this.updateSyncUI(); 997 }, 998 999 openContentInBrowser(url, options) { 1000 let win = Services.wm.getMostRecentWindow("navigator:browser"); 1001 if (!win) { 1002 openTrustedLinkIn(url, "tab"); 1003 return; 1004 } 1005 win.switchToTabHavingURI(url, true, options); 1006 }, 1007 1008 // Replace the current tab with the specified URL. 1009 replaceTabWithUrl(url) { 1010 // Get the <browser> element hosting us. 1011 let browser = window.docShell.chromeEventHandler; 1012 // And tell it to load our URL. 1013 browser.loadURI(Services.io.newURI(url), { 1014 triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( 1015 {} 1016 ), 1017 }); 1018 }, 1019 1020 clickOrSpaceOrEnterPressed(event) { 1021 // Note: charCode is deprecated, but 'char' not yet implemented. 1022 // Replace charCode with char when implemented, see Bug 680830 1023 return ( 1024 (event.type == "click" && event.button == 0) || 1025 (event.type == "keypress" && 1026 (event.charCode == KeyEvent.DOM_VK_SPACE || 1027 event.keyCode == KeyEvent.DOM_VK_RETURN)) 1028 ); 1029 }, 1030 1031 openChangeProfileImage(event) { 1032 if (this.clickOrSpaceOrEnterPressed(event)) { 1033 FxAccounts.config 1034 .promiseChangeAvatarURI(SyncHelpers.getEntryPoint()) 1035 .then(url => { 1036 this.openContentInBrowser(url, { 1037 replaceQueryString: true, 1038 triggeringPrincipal: 1039 Services.scriptSecurityManager.getSystemPrincipal(), 1040 }); 1041 }); 1042 // Prevent page from scrolling on the space key. 1043 event.preventDefault(); 1044 } 1045 }, 1046 1047 pairAnotherDevice() { 1048 gSubDialog.open( 1049 "chrome://browser/content/preferences/fxaPairDevice.xhtml", 1050 { features: "resizable=no" } 1051 ); 1052 }, 1053 1054 _populateComputerName(value) { 1055 let textbox = document.getElementById("fxaSyncComputerName"); 1056 if (!textbox.hasAttribute("placeholder")) { 1057 textbox.setAttribute( 1058 "placeholder", 1059 fxAccounts.device.getDefaultLocalName() 1060 ); 1061 } 1062 textbox.value = value; 1063 }, 1064 1065 // arranges to dynamically show or hide sync engine name elements based on the 1066 // preferences used for this engines. 1067 setupEnginesUI() { 1068 let observe = (elt, prefName) => { 1069 elt.hidden = !Services.prefs.getBoolPref(prefName, false); 1070 }; 1071 1072 for (let elt of document.querySelectorAll("[engine_preference]")) { 1073 let prefName = elt.getAttribute("engine_preference"); 1074 let obs = observe.bind(null, elt, prefName); 1075 obs(); 1076 Services.prefs.addObserver(prefName, obs); 1077 window.addEventListener("unload", () => { 1078 Services.prefs.removeObserver(prefName, obs); 1079 }); 1080 } 1081 }, 1082 };