/* 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/. */ var gFxAccounts = { SYNC_MIGRATION_NOTIFICATION_TITLE: "fxa-migration", _initialized: false, _inCustomizationMode: false, _cachedProfile: null, get weave() { delete this.weave; return this.weave = Cc["@mozilla.org/weave/service;1"] .getService(Ci.nsISupports) .wrappedJSObject; }, get topics() { // Do all this dance to lazy-load FxAccountsCommon. delete this.topics; return this.topics = [ "weave:service:ready", "weave:service:login:change", "weave:service:setup-complete", "weave:service:sync:error", "weave:ui:login:error", "fxa-migration:state-changed", this.FxAccountsCommon.ONLOGIN_NOTIFICATION, this.FxAccountsCommon.ONLOGOUT_NOTIFICATION, this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION, ]; }, get panelUIFooter() { delete this.panelUIFooter; return this.panelUIFooter = document.getElementById("PanelUI-footer-fxa"); }, get panelUIStatus() { delete this.panelUIStatus; return this.panelUIStatus = document.getElementById("PanelUI-fxa-status"); }, get panelUIAvatar() { delete this.panelUIAvatar; return this.panelUIAvatar = document.getElementById("PanelUI-fxa-avatar"); }, get panelUILabel() { delete this.panelUILabel; return this.panelUILabel = document.getElementById("PanelUI-fxa-label"); }, get panelUIIcon() { delete this.panelUIIcon; return this.panelUIIcon = document.getElementById("PanelUI-fxa-icon"); }, get strings() { delete this.strings; return this.strings = Services.strings.createBundle( "chrome://browser/locale/accounts.properties" ); }, get loginFailed() { // Referencing Weave.Service will implicitly initialize sync, and we don't // want to force that - so first check if it is ready. let service = Cc["@mozilla.org/weave/service;1"] .getService(Components.interfaces.nsISupports) .wrappedJSObject; if (!service.ready) { return false; } // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in". // All other login failures are assumed to be transient and should go // away by themselves, so aren't reflected here. return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED; }, get sendTabToDeviceEnabled() { return Services.prefs.getBoolPref("services.sync.sendTabToDevice.enabled"); }, get remoteClients() { return Weave.Service.clientsEngine.remoteClients .sort((a, b) => a.name.localeCompare(b.name)); }, init: function () { // Bail out if we're already initialized and for pop-up windows. if (this._initialized || !window.toolbar.visible) { return; } for (let topic of this.topics) { Services.obs.addObserver(this, topic, false); } gNavToolbox.addEventListener("customizationstarting", this); gNavToolbox.addEventListener("customizationending", this); EnsureFxAccountsWebChannel(); this._initialized = true; this.updateUI(); }, uninit: function () { if (!this._initialized) { return; } for (let topic of this.topics) { Services.obs.removeObserver(this, topic); } this._initialized = false; }, observe: function (subject, topic, data) { switch (topic) { case "fxa-migration:state-changed": this.onMigrationStateChanged(data, subject); break; case this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION: this._cachedProfile = null; // Fallthrough intended default: this.updateUI(); break; } }, onMigrationStateChanged: function () { // Since we nuked most of the migration code, this notification will fire // once after legacy Sync has been disconnected (and should never fire // again) let nb = window.document.getElementById("global-notificationbox"); let msg = this.strings.GetStringFromName("autoDisconnectDescription") let signInLabel = this.strings.GetStringFromName("autoDisconnectSignIn.label"); let signInAccessKey = this.strings.GetStringFromName("autoDisconnectSignIn.accessKey"); let learnMoreLink = this.fxaMigrator.learnMoreLink; let buttons = [ { label: signInLabel, accessKey: signInAccessKey, callback: () => { this.openPreferences(); } } ]; let fragment = document.createDocumentFragment(); let msgNode = document.createTextNode(msg); fragment.appendChild(msgNode); if (learnMoreLink) { let link = document.createElement("label"); link.className = "text-link"; link.setAttribute("value", learnMoreLink.text); link.href = learnMoreLink.href; fragment.appendChild(link); } nb.appendNotification(fragment, this.SYNC_MIGRATION_NOTIFICATION_TITLE, undefined, nb.PRIORITY_WARNING_LOW, buttons); // ensure the hamburger menu reflects the newly disconnected state. this.updateAppMenuItem(); }, handleEvent: function (event) { this._inCustomizationMode = event.type == "customizationstarting"; this.updateAppMenuItem(); }, updateUI: function () { // It's possible someone signed in to FxA after seeing our notification // about "Legacy Sync migration" (which now is actually "Legacy Sync // auto-disconnect") so kill that notification if it still exists. let nb = window.document.getElementById("global-notificationbox"); let n = nb.getNotificationWithValue(this.SYNC_MIGRATION_NOTIFICATION_TITLE); if (n) { nb.removeNotification(n, true); } this.updateAppMenuItem(); }, // Note that updateAppMenuItem() returns a Promise that's only used by tests. updateAppMenuItem: function () { let profileInfoEnabled = false; try { profileInfoEnabled = Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled"); } catch (e) { } // Bail out if FxA is disabled. if (!this.weave.fxAccountsEnabled) { return Promise.resolve(); } this.panelUIFooter.hidden = false; // Make sure the button is disabled in customization mode. if (this._inCustomizationMode) { this.panelUIStatus.setAttribute("disabled", "true"); this.panelUILabel.setAttribute("disabled", "true"); this.panelUIAvatar.setAttribute("disabled", "true"); this.panelUIIcon.setAttribute("disabled", "true"); } else { this.panelUIStatus.removeAttribute("disabled"); this.panelUILabel.removeAttribute("disabled"); this.panelUIAvatar.removeAttribute("disabled"); this.panelUIIcon.removeAttribute("disabled"); } let defaultLabel = this.panelUIStatus.getAttribute("defaultlabel"); let errorLabel = this.panelUIStatus.getAttribute("errorlabel"); let unverifiedLabel = this.panelUIStatus.getAttribute("unverifiedlabel"); // The localization string is for the signed in text, but it's the default text as well let defaultTooltiptext = this.panelUIStatus.getAttribute("signedinTooltiptext"); let updateWithUserData = (userData) => { // Window might have been closed while fetching data. if (window.closed) { return; } // Reset the button to its original state. this.panelUILabel.setAttribute("label", defaultLabel); this.panelUIStatus.setAttribute("tooltiptext", defaultTooltiptext); this.panelUIFooter.removeAttribute("fxastatus"); this.panelUIFooter.removeAttribute("fxaprofileimage"); this.panelUIAvatar.style.removeProperty("list-style-image"); let showErrorBadge = false; if (userData) { // At this point we consider the user as logged-in (but still can be in an error state) if (this.loginFailed) { let tooltipDescription = this.strings.formatStringFromName("reconnectDescription", [userData.email], 1); this.panelUIFooter.setAttribute("fxastatus", "error"); this.panelUILabel.setAttribute("label", errorLabel); this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription); showErrorBadge = true; } else if (!userData.verified) { let tooltipDescription = this.strings.formatStringFromName("verifyDescription", [userData.email], 1); this.panelUIFooter.setAttribute("fxastatus", "error"); this.panelUIFooter.setAttribute("unverified", "true"); this.panelUILabel.setAttribute("label", unverifiedLabel); this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription); showErrorBadge = true; } else { this.panelUIFooter.setAttribute("fxastatus", "signedin"); this.panelUILabel.setAttribute("label", userData.email); } if (profileInfoEnabled) { this.panelUIFooter.setAttribute("fxaprofileimage", "enabled"); } } if (showErrorBadge) { gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication"); } else { gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_FXA); } } let updateWithProfile = (profile) => { if (profileInfoEnabled) { if (profile.displayName) { this.panelUILabel.setAttribute("label", profile.displayName); } if (profile.avatar) { this.panelUIFooter.setAttribute("fxaprofileimage", "set"); let bgImage = "url(\"" + profile.avatar + "\")"; this.panelUIAvatar.style.listStyleImage = bgImage; let img = new Image(); img.onerror = () => { // Clear the image if it has trouble loading. Since this callback is asynchronous // we check to make sure the image is still the same before we clear it. if (this.panelUIAvatar.style.listStyleImage === bgImage) { this.panelUIFooter.removeAttribute("fxaprofileimage"); this.panelUIAvatar.style.removeProperty("list-style-image"); } }; img.src = profile.avatar; } } } return fxAccounts.getSignedInUser().then(userData => { // userData may be null here when the user is not signed-in, but that's expected updateWithUserData(userData); // unverified users cause us to spew log errors fetching an OAuth token // to fetch the profile, so don't even try in that case. if (!userData || !userData.verified || !profileInfoEnabled) { return null; // don't even try to grab the profile. } if (this._cachedProfile) { return this._cachedProfile; } return fxAccounts.getSignedInUserProfile().catch(err => { // Not fetching the profile is sad but the FxA logs will already have noise. return null; }); }).then(profile => { if (!profile) { return; } updateWithProfile(profile); this._cachedProfile = profile; // Try to avoid fetching the profile on every UI update }).catch(error => { // This is most likely in tests, were we quickly log users in and out. // The most likely scenario is a user logged out, so reflect that. // Bug 995134 calls for better errors so we could retry if we were // sure this was the failure reason. this.FxAccountsCommon.log.error("Error updating FxA account info", error); updateWithUserData(null); }); }, onMenuPanelCommand: function () { switch (this.panelUIFooter.getAttribute("fxastatus")) { case "signedin": this.openPreferences(); break; case "error": if (this.panelUIFooter.getAttribute("unverified")) { this.openPreferences(); } else { this.openSignInAgainPage("menupanel"); } break; default: this.openPreferences(); break; } PanelUI.hide(); }, openPreferences: function () { openPreferences("paneSync", { urlParams: { entrypoint: "menupanel" } }); }, openAccountsPage: function (action, urlParams={}) { let params = new URLSearchParams(); if (action) { params.set("action", action); } for (let name in urlParams) { if (urlParams[name] !== undefined) { params.set(name, urlParams[name]); } } let url = "about:accounts?" + params; switchToTabHavingURI(url, true, { replaceQueryString: true }); }, openSignInAgainPage: function (entryPoint) { this.openAccountsPage("reauth", { entrypoint: entryPoint }); }, sendTabToDevice: function (url, clientId, title) { Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title); }, populateSendTabToDevicesMenu: function (devicesPopup, url, title) { // remove existing menu items while (devicesPopup.hasChildNodes()) { devicesPopup.removeChild(devicesPopup.firstChild); } const fragment = document.createDocumentFragment(); const onTargetDeviceCommand = (event) => { const clientId = event.target.getAttribute("clientId"); const clients = clientId ? [clientId] : this.remoteClients.map(client => client.id); clients.forEach(clientId => this.sendTabToDevice(url, clientId, title)); } function addTargetDevice(clientId, name) { const targetDevice = document.createElement("menuitem"); targetDevice.addEventListener("command", onTargetDeviceCommand, true); targetDevice.setAttribute("class", "sendtab-target"); targetDevice.setAttribute("clientId", clientId); targetDevice.setAttribute("label", name); fragment.appendChild(targetDevice); } const clients = this.remoteClients; for (let client of clients) { addTargetDevice(client.id, client.name); } // "All devices" menu item if (clients.length > 1) { const separator = document.createElement("menuseparator"); fragment.appendChild(separator); const allDevicesLabel = this.strings.GetStringFromName("sendTabToAllDevices.menuitem"); addTargetDevice("", allDevicesLabel); } devicesPopup.appendChild(fragment); }, updateTabContextMenu: function (aPopupMenu) { if (!this.sendTabToDeviceEnabled) { return; } const remoteClientPresent = this.remoteClients.length > 0; ["context_sendTabToDevice", "context_sendTabToDevice_separator"] .forEach(id => { document.getElementById(id).hidden = !remoteClientPresent }); }, initPageContextMenu: function (contextMenu) { if (!this.sendTabToDeviceEnabled) { return; } const remoteClientPresent = this.remoteClients.length > 0; // showSendLink and showSendPage are mutually exclusive const showSendLink = remoteClientPresent && (contextMenu.onSaveableLink || contextMenu.onPlainTextLink); const showSendPage = !showSendLink && remoteClientPresent && !(contextMenu.isContentSelected || contextMenu.onImage || contextMenu.onCanvas || contextMenu.onVideo || contextMenu.onAudio || contextMenu.onLink || contextMenu.onTextInput); ["context-sendpagetodevice", "context-sep-sendpagetodevice"] .forEach(id => contextMenu.showItem(id, showSendPage)); ["context-sendlinktodevice", "context-sep-sendlinktodevice"] .forEach(id => contextMenu.showItem(id, showSendLink)); } }; XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function () { return Cu.import("resource://gre/modules/FxAccountsCommon.js", {}); }); XPCOMUtils.defineLazyModuleGetter(gFxAccounts, "fxaMigrator", "resource://services-sync/FxaMigrator.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel", "resource://gre/modules/FxAccountsWebChannel.jsm");