diff options
Diffstat (limited to 'comm/mail/components/im/content/imAccounts.js')
-rw-r--r-- | comm/mail/components/im/content/imAccounts.js | 663 |
1 files changed, 663 insertions, 0 deletions
diff --git a/comm/mail/components/im/content/imAccounts.js b/comm/mail/components/im/content/imAccounts.js new file mode 100644 index 0000000000..46bb72c197 --- /dev/null +++ b/comm/mail/components/im/content/imAccounts.js @@ -0,0 +1,663 @@ +/* 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/. */ + +/* globals MozElements */ +/* globals statusSelector */ +/* globals MsgAccountManager */ + +var { DownloadUtils } = ChromeUtils.importESModule( + "resource://gre/modules/DownloadUtils.sys.mjs" +); + +var { IMServices } = ChromeUtils.importESModule( + "resource:///modules/IMServices.sys.mjs" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +ChromeUtils.defineESModuleGetters(this, { + PluralForm: "resource://gre/modules/PluralForm.sys.mjs", +}); + +// This is the list of notifications that the account manager window observes +var events = [ + "prpl-quit", + "account-list-updated", + "account-added", + "account-updated", + "account-removed", + "account-connected", + "account-connecting", + "account-disconnected", + "account-disconnecting", + "account-connect-progress", + "account-connect-error", + "autologin-processed", + "status-changed", + "network:offline-status-changed", +]; + +var gAccountManager = { + // Sets the delay after connect() or disconnect() during which + // it is impossible to perform disconnect() and connect() + _disabledDelay: 500, + disableTimerID: 0, + _connectedLabelInterval: 0, + + get msgNotificationBar() { + if (!this._notificationBox) { + this._notificationBox = new MozElements.NotificationBox(element => { + document.getElementById("accounts-notification-box").prepend(element); + }); + } + return this._notificationBox; + }, + + load() { + // Wait until the password service is ready before offering anything. + Services.logins.initializationPromise.then( + () => { + this.accountList = document.getElementById("accountlist"); + let defaultID; + IMServices.core.init(); // ensure the imCore is initialized. + for (let acc of this.getAccounts()) { + let elt = document.createXULElement("richlistitem", { + is: "chat-account-richlistitem", + }); + this.accountList.appendChild(elt); + elt.build(acc); + if ( + !defaultID && + acc.firstConnectionState == acc.FIRST_CONNECTION_CRASHED + ) { + defaultID = acc.id; + } + } + for (let event of events) { + Services.obs.addObserver(this, event); + } + if (!this.accountList.getRowCount()) { + // This is horrible, but it works. Otherwise (at least on mac) + // the wizard is not centered relatively to the account manager + setTimeout(function () { + gAccountManager.new(); + }, 0); + } else { + // we have accounts, show the list + document.getElementById("noAccountScreen").hidden = true; + document.getElementById("accounts-notification-box").hidden = false; + + // ensure an account is selected + if (defaultID) { + this.selectAccount(defaultID); + } else { + this.accountList.selectedIndex = 0; + } + } + + this.setAutoLoginNotification(); + + this.accountList.addEventListener("keypress", this.onKeyPress, true); + window.addEventListener("unload", this.unload.bind(this)); + this._connectedLabelInterval = setInterval( + this.updateConnectedLabels, + 60000 + ); + statusSelector.init(); + }, + () => { + this.close(); + } + ); + }, + unload() { + clearInterval(this._connectedLabelInterval); + for (let event of events) { + Services.obs.removeObserver(this, event); + } + }, + _updateAccountList() { + let accountList = this.accountList; + let i = 0; + for (let acc of this.getAccounts()) { + let oldItem = accountList.getItemAtIndex(i); + if (oldItem.id != acc.id) { + let accElt = document.getElementById(acc.id); + accountList.insertBefore(accElt, oldItem); + accElt.refreshState(); + } + ++i; + } + + if (accountList.itemCount == 0) { + // Focus the "New Account" button if there are no accounts left. + document.getElementById("newaccount").focus(); + // Return early, otherwise we'll run into an 'undefined property' strict + // warning when trying to focus the buttons. Fixes bug 408. + return; + } + + // The selected item is still selected + if (accountList.selectedItem) { + accountList.selectedItem.setFocus(); + } + accountList.ensureSelectedElementIsVisible(); + + // We need to refresh the disabled menu items + this.disableCommandItems(); + }, + observe(aObject, aTopic, aData) { + if (aTopic == "prpl-quit") { + // libpurple is being uninitialized. We don't need the account + // manager window anymore, close it. + this.close(); + return; + } else if (aTopic == "autologin-processed") { + let notification = + this.msgNotificationBar.getNotificationWithValue("autoLoginStatus"); + if (notification) { + notification.close(); + } + return; + } else if (aTopic == "network:offline-status-changed") { + this.setOffline(aData == "offline"); + return; + } else if (aTopic == "status-changed") { + this.setOffline(aObject.statusType == Ci.imIStatusInfo.STATUS_OFFLINE); + return; + } else if (aTopic == "account-list-updated") { + this._updateAccountList(); + return; + } + + // The following notification handlers need an account. + let account = aObject.QueryInterface(Ci.imIAccount); + + if (aTopic == "account-added") { + document.getElementById("noAccountScreen").hidden = true; + document.getElementById("accounts-notification-box").hidden = false; + let elt = document.createXULElement("richlistitem", { + is: "chat-account-richlistitem", + }); + this.accountList.appendChild(elt); + elt.build(account); + if (this.accountList.getRowCount() == 1) { + this.accountList.selectedIndex = 0; + } + } else if (aTopic == "account-removed") { + let elt = document.getElementById(account.id); + elt.destroy(); + if (!elt.selected) { + elt.remove(); + return; + } + // The currently selected element is removed, + // ensure another element gets selected (if the list is not empty) + var selectedIndex = this.accountList.selectedIndex; + // Prevent errors if the timer is active and the account deleted + clearTimeout(this.disableTimerID); + this.disableTimerID = 0; + elt.remove(); + var count = this.accountList.getRowCount(); + if (!count) { + document.getElementById("noAccountScreen").hidden = false; + document.getElementById("accounts-notification-box").hidden = true; + return; + } + if (selectedIndex == count) { + --selectedIndex; + } + this.accountList.selectedIndex = selectedIndex; + } else if (aTopic == "account-updated") { + document.getElementById(account.id).build(account); + this.disableCommandItems(); + } else if (aTopic == "account-connect-progress") { + document.getElementById(account.id).updateConnectingProgress(); + } else if (aTopic == "account-connect-error") { + document.getElementById(account.id).updateConnectionError(); + // See NSSErrorsService::ErrorIsOverridable. + if ( + [ + "MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED", + "MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY", + "MOZILLA_PKIX_ERROR_EMPTY_ISSUER_NAME", + "MOZILLA_PKIX_ERROR_INADEQUATE_KEY_SIZE", + "MOZILLA_PKIX_ERROR_MITM_DETECTED", + "MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE", + "MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE", + "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT", + "MOZILLA_PKIX_ERROR_V1_CERT_USED_AS_CA", + "SEC_ERROR_CA_CERT_INVALID", + "SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED", + "SEC_ERROR_EXPIRED_CERTIFICATE", + "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE", + "SEC_ERROR_INVALID_TIME", + "SEC_ERROR_UNKNOWN_ISSUER", + "SSL_ERROR_BAD_CERT_DOMAIN", + ].includes(account.prplAccount.securityInfo?.errorCodeString) + ) { + this.addException(); + } + } else { + const stateEvents = { + "account-connected": "connected", + "account-connecting": "connecting", + "account-disconnected": "disconnected", + "account-disconnecting": "disconnecting", + }; + if (aTopic in stateEvents) { + let elt = document.getElementById(account.id); + if (!elt) { + // Probably disconnecting a removed account. + return; + } + elt.refreshState(stateEvents[aTopic]); + } + } + }, + cancelReconnection() { + this.accountList.selectedItem.cancelReconnection(); + }, + connect() { + let account = this.accountList.selectedItem.account; + if (account.disconnected) { + this.temporarilyDisableButtons(); + account.connect(); + } + }, + disconnect() { + let account = this.accountList.selectedItem.account; + if (account.connected || account.connecting) { + this.temporarilyDisableButtons(); + account.disconnect(); + } + }, + addException() { + let account = this.accountList.selectedItem.account; + let prplAccount = account.prplAccount; + if (!prplAccount.connectionTarget) { + return; + } + + // Open the Gecko SSL exception dialog. + let params = { + exceptionAdded: false, + securityInfo: prplAccount.securityInfo, + prefetchCert: true, + location: prplAccount.connectionTarget, + }; + window.openDialog( + "chrome://pippki/content/exceptionDialog.xhtml", + "", + "chrome,centerscreen,modal", + params + ); + // Reconnect the account if an exception was added. + if (params.exceptionAdded) { + account.disconnect(); + account.connect(); + } + }, + copyDebugLog() { + let account = this.accountList.selectedItem.account; + let text = account + .getDebugMessages() + .map(function (dbgMsg) { + let m = dbgMsg.message; + let time = new Date(m.timeStamp); + const dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "short", + timeStyle: "long", + }); + time = dateTimeFormatter.format(time); + let level = dbgMsg.logLevel; + if (!level) { + return "(" + m.errorMessage + ")"; + } + if (level == dbgMsg.LEVEL_ERROR) { + level = "ERROR"; + } else if (level == dbgMsg.LEVEL_WARNING) { + level = "WARN."; + } else if (level == dbgMsg.LEVEL_LOG) { + level = "LOG "; + } else { + level = "DEBUG"; + } + return ( + "[" + + time + + "] " + + level + + " (@ " + + m.sourceLine + + " " + + m.sourceName + + ":" + + m.lineNumber + + ")\n" + + m.errorMessage + ); + }) + .join("\n"); + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(text); + }, + updateConnectedLabels() { + for (let i = 0; i < gAccountManager.accountList.itemCount; ++i) { + let item = gAccountManager.accountList.getItemAtIndex(i); + if (item.account.connected) { + item.refreshConnectedLabel(); + } + } + }, + /* This function disables the connect/disconnect buttons for + * `this._disabledDelay` ms before calling disableCommandItems to restore + * the state of the buttons. + */ + temporarilyDisableButtons() { + document.getElementById("cmd_disconnect").setAttribute("disabled", "true"); + document.getElementById("cmd_connect").setAttribute("disabled", "true"); + clearTimeout(this.disableTimerID); + this.accountList.focus(); + this.disableTimerID = setTimeout( + function (aItem) { + gAccountManager.disableTimerID = 0; + gAccountManager.disableCommandItems(); + aItem.setFocus(); + }, + this._disabledDelay, + this.accountList.selectedItem + ); + }, + + new() { + this.openDialog("chrome://messenger/content/chat/imAccountWizard.xhtml"); + }, + edit() { + // Find the nsIIncomingServer for the current imIAccount. + let server = null; + let imAccountId = this.accountList.selectedItem.account.numericId; + for (let account of MailServices.accounts.accounts) { + let incomingServer = account.incomingServer; + if (!incomingServer || incomingServer.type != "im") { + continue; + } + if (incomingServer.wrappedJSObject.imAccount.numericId == imAccountId) { + server = incomingServer; + break; + } + } + + MsgAccountManager(null, server); + }, + autologin() { + var elt = this.accountList.selectedItem; + elt.autoLogin = !elt.autoLogin; + }, + close() { + // If a modal dialog is opened, we can't close this window now + if (this.modalDialog) { + setTimeout(function () { + window.close(); + }, 0); + } else { + window.close(); + } + }, + + /* This function disables or enables the currently selected button and + the corresponding context menu item */ + disableCommandItems() { + let accountList = this.accountList; + let selectedItem = accountList.selectedItem; + // When opening the account manager, if accounts have errors, we + // can be called during build(), before any item is selected. + // In this case, just return early. + if (!selectedItem) { + return; + } + + // If the timer that disables the button (for a short time) already exists, + // we don't want to interfere and set the button as enabled. + if (this.disableTimerID) { + return; + } + + let account = selectedItem.account; + let isCommandDisabled = + this.isOffline || + (account.disconnected && + account.connectionErrorReason == Ci.imIAccount.ERROR_UNKNOWN_PRPL); + + let disabledItems = ["connect", "disconnect"]; + for (let name of disabledItems) { + let elt = document.getElementById("cmd_" + name); + if (isCommandDisabled) { + elt.setAttribute("disabled", "true"); + } else { + elt.removeAttribute("disabled"); + } + } + }, + onContextMenuShowing(event) { + let targetElt = event.target.triggerNode.closest( + 'richlistitem[is="chat-account-richlistitem"]' + ); + document.querySelectorAll(".im-context-account-item").forEach(e => { + e.hidden = !targetElt; + }); + if (targetElt) { + let account = targetElt.account; + let hiddenItems = { + connect: !account.disconnected, + disconnect: account.disconnected || account.disconnecting, + cancelReconnection: !targetElt.hasAttribute("reconnectPending"), + accountsItemsSeparator: account.disconnecting, + }; + for (let name in hiddenItems) { + document.getElementById("context_" + name).hidden = hiddenItems[name]; + } + } + }, + + selectAccount(aAccountId) { + this.accountList.selectedItem = document.getElementById(aAccountId); + this.accountList.ensureSelectedElementIsVisible(); + }, + onAccountSelect() { + clearTimeout(this.disableTimerID); + this.disableTimerID = 0; + this.disableCommandItems(); + // Horrible hack here too, see Bug 177 + setTimeout( + function (aThis) { + try { + aThis.accountList.selectedItem.setFocus(); + } catch (e) { + /* Sometimes if the user goes too fast with VK_UP or VK_DOWN, the + selectedItem doesn't have the expected binding attached */ + } + }, + 0, + this + ); + }, + + onKeyPress(event) { + if (!this.selectedItem) { + return; + } + // As we stop propagation, the default action applies to the richlistbox + // so that the selected account is changed with this default action + if (event.keyCode == event.DOM_VK_DOWN) { + if (this.selectedIndex < this.itemCount - 1) { + this.ensureIndexIsVisible(this.selectedIndex + 1); + } + event.stopPropagation(); + return; + } + + if (event.keyCode == event.DOM_VK_UP) { + if (this.selectedIndex > 0) { + this.ensureIndexIsVisible(this.selectedIndex - 1); + } + event.stopPropagation(); + return; + } + + if (event.keyCode == event.DOM_VK_RETURN) { + let target = event.target; + if ( + target.localName != "checkbox" && + (target.localName != "button" || + /^(dis)?connect$/.test(target.getAttribute("anonid"))) + ) { + this.selectedItem.buttons.proceedDefaultAction(); + } + } + }, + + *getAccounts() { + for (let account of IMServices.accounts.getAccounts()) { + yield account; + } + }, + + openDialog(aUrl, aArgs) { + this.modalDialog = true; + window.openDialog(aUrl, "", "chrome,modal,titlebar,centerscreen", aArgs); + this.modalDialog = false; + }, + + setAutoLoginNotification() { + var as = IMServices.accounts; + var autoLoginStatus = as.autoLoginStatus; + let isOffline = false; + let crashCount = 0; + for (let acc of this.getAccounts()) { + if ( + acc.autoLogin && + acc.firstConnectionState == acc.FIRST_CONNECTION_CRASHED + ) { + ++crashCount; + } + } + + if (autoLoginStatus == as.AUTOLOGIN_ENABLED && crashCount == 0) { + let status = IMServices.core.globalUserStatus.statusType; + this.setOffline(isOffline || status == Ci.imIStatusInfo.STATUS_OFFLINE); + return; + } + + var bundle = document.getElementById("accountsBundle"); + let box = this.msgNotificationBar; + var prio = box.PRIORITY_INFO_HIGH; + var connectNowButton = { + accessKey: bundle.getString( + "accountsManager.notification.button.accessKey" + ), + callback: this.processAutoLogin, + label: bundle.getString("accountsManager.notification.button.label"), + }; + var barLabel; + + switch (autoLoginStatus) { + case as.AUTOLOGIN_USER_DISABLED: + barLabel = bundle.getString( + "accountsManager.notification.userDisabled.label" + ); + break; + + case as.AUTOLOGIN_SAFE_MODE: + barLabel = bundle.getString( + "accountsManager.notification.safeMode.label" + ); + break; + + case as.AUTOLOGIN_START_OFFLINE: + barLabel = bundle.getString( + "accountsManager.notification.startOffline.label" + ); + isOffline = true; + break; + + case as.AUTOLOGIN_CRASH: + barLabel = bundle.getString("accountsManager.notification.crash.label"); + prio = box.PRIORITY_WARNING_MEDIUM; + break; + + /* One or more accounts made the application crash during their connection. + If none, this function has already returned */ + case as.AUTOLOGIN_ENABLED: + barLabel = bundle.getString( + "accountsManager.notification.singleCrash.label" + ); + barLabel = PluralForm.get(crashCount, barLabel).replace( + "#1", + crashCount + ); + prio = box.PRIORITY_WARNING_MEDIUM; + connectNowButton.callback = this.processCrashedAccountsLogin; + break; + + default: + barLabel = bundle.getString("accountsManager.notification.other.label"); + } + let status = IMServices.core.globalUserStatus.statusType; + this.setOffline(isOffline || status == Ci.imIStatusInfo.STATUS_OFFLINE); + + box.appendNotification( + "autologinStatus", + { + label: barLabel, + priority: prio, + }, + [connectNowButton] + ); + }, + processAutoLogin() { + var ioService = Services.io; + if (ioService.offline) { + ioService.manageOfflineStatus = false; + ioService.offline = false; + } + + IMServices.accounts.processAutoLogin(); + + gAccountManager.accountList.selectedItem.setFocus(); + }, + processCrashedAccountsLogin() { + for (let acc in gAccountManager.getAccounts()) { + if ( + acc.disconnected && + acc.autoLogin && + acc.firstConnectionState == acc.FIRST_CONNECTION_CRASHED + ) { + acc.connect(); + } + } + + let notification = + this.msgNotificationBar.getNotificationWithValue("autoLoginStatus"); + if (notification) { + notification.close(); + } + + gAccountManager.accountList.selectedItem.setFocus(); + }, + setOffline(aState) { + this.isOffline = aState; + if (aState) { + this.accountList.setAttribute("offline", "true"); + } else { + this.accountList.removeAttribute("offline"); + } + this.disableCommandItems(); + }, +}; + +window.addEventListener("DOMContentLoaded", () => { + gAccountManager.load(); +}); |