diff options
Diffstat (limited to 'application/basilisk/base/content/browser-fxaccounts.js')
-rw-r--r-- | application/basilisk/base/content/browser-fxaccounts.js | 459 |
1 files changed, 459 insertions, 0 deletions
diff --git a/application/basilisk/base/content/browser-fxaccounts.js b/application/basilisk/base/content/browser-fxaccounts.js new file mode 100644 index 000000000..0bbce3e26 --- /dev/null +++ b/application/basilisk/base/content/browser-fxaccounts.js @@ -0,0 +1,459 @@ +/* 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"); |