diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/chat/components/src/imAccounts.sys.mjs | |
parent | Initial commit. (diff) | |
download | thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.tar.xz thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/chat/components/src/imAccounts.sys.mjs')
-rw-r--r-- | comm/chat/components/src/imAccounts.sys.mjs | 1237 |
1 files changed, 1237 insertions, 0 deletions
diff --git a/comm/chat/components/src/imAccounts.sys.mjs b/comm/chat/components/src/imAccounts.sys.mjs new file mode 100644 index 0000000000..f06b503fa6 --- /dev/null +++ b/comm/chat/components/src/imAccounts.sys.mjs @@ -0,0 +1,1237 @@ +/* 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/. */ + +import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { + ClassInfo, + executeSoon, + l10nHelper, +} from "resource:///modules/imXPCOMUtils.sys.mjs"; + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +import { IMServices } from "resource:///modules/IMServices.sys.mjs"; +import { + GenericAccountPrototype, + GenericAccountBuddyPrototype, +} from "resource:///modules/jsProtoHelper.sys.mjs"; + +const lazy = {}; +XPCOMUtils.defineLazyGetter(lazy, "_", () => + l10nHelper("chrome://chat/locale/accounts.properties") +); +XPCOMUtils.defineLazyGetter(lazy, "_maxDebugMessages", () => + Services.prefs.getIntPref("messenger.accounts.maxDebugMessages") +); +XPCOMUtils.defineLazyServiceGetter( + lazy, + "HttpProtocolHandler", + "@mozilla.org/network/protocol;1?name=http", + "nsIHttpProtocolHandler" +); + +var kPrefAutologinPending = "messenger.accounts.autoLoginPending"; +let kPrefAccountOrder = "mail.accountmanager.accounts"; +var kPrefAccountPrefix = "messenger.account."; +var kAccountKeyPrefix = "account"; +var kAccountOptionPrefPrefix = "options."; +var kPrefAccountName = "name"; +var kPrefAccountPrpl = "prpl"; +var kPrefAccountAutoLogin = "autoLogin"; +var kPrefAccountAutoJoin = "autoJoin"; +var kPrefAccountAlias = "alias"; +var kPrefAccountFirstConnectionState = "firstConnectionState"; + +var gUserCanceledPrimaryPasswordPrompt = false; + +var SavePrefTimer = { + saveNow() { + if (this._timer) { + clearTimeout(this._timer); + this._timer = null; + } + Services.prefs.savePrefFile(null); + }, + _timer: null, + unInitTimer() { + if (this._timer) { + this.saveNow(); + } + }, + initTimer() { + if (!this._timer) { + this._timer = setTimeout(this.saveNow.bind(this), 5000); + } + }, +}; + +var AutoLoginCounter = { + _count: 0, + startAutoLogin() { + ++this._count; + if (this._count != 1) { + return; + } + Services.prefs.setIntPref(kPrefAutologinPending, Date.now() / 1000); + SavePrefTimer.saveNow(); + }, + finishedAutoLogin() { + --this._count; + if (this._count != 0) { + return; + } + Services.prefs.clearUserPref(kPrefAutologinPending); + SavePrefTimer.initTimer(); + }, +}; + +function UnknownProtocol(aPrplId) { + this.id = aPrplId; +} +UnknownProtocol.prototype = { + __proto__: ClassInfo("prplIProtocol", "Unknown protocol"), + get name() { + return ""; + }, + get normalizedName() { + // Use the ID, but remove the 'prpl-' prefix. + return this.id.replace(/^prpl-/, ""); + }, + get iconBaseURI() { + return "chrome://chat/skin/prpl-unknown/"; + }, + getOptions() { + return []; + }, + get usernamePrefix() { + return ""; + }, + getUsernameSplit() { + return []; + }, + get usernameEmptyText() { + return ""; + }, + + getAccount(aKey, aName) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + accountExists() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + + // false seems an acceptable default for all options + // (they should never be called anyway). + get chatHasTopic() { + return false; + }, + get noPassword() { + return false; + }, + get passwordOptional() { + return true; + }, + get slashCommandsNative() { + return false; + }, + get canEncrypt() { + return false; + }, +}; + +// An unknown prplIAccount. +function UnknownAccount(aAccount) { + this._init(aAccount.protocol, aAccount); +} +UnknownAccount.prototype = GenericAccountPrototype; + +function UnknownAccountBuddy(aAccount, aBuddy, aTag) { + this._init(new UnknownAccount(aAccount), aBuddy, aTag); +} +UnknownAccountBuddy.prototype = GenericAccountBuddyPrototype; + +/** + * @param {string} aKey - Account key for preferences. + * @param {string} [aName] - Name of the account if it is new. Will be stored + * in account preferences. If not provided, the value from the account + * preferences is used instead. + * @param {string} [aPrplId] - Protocol ID for this account if it is new. Will + * be stored in account preferences. If not provided, the value from the + * account preferences is used instead. + */ +function imAccount(aKey, aName, aPrplId) { + if (!aKey.startsWith(kAccountKeyPrefix)) { + throw Components.Exception(`Invalid key: ${aKey}`, Cr.NS_ERROR_INVALID_ARG); + } + + this.id = aKey; + this.numericId = parseInt(aKey.substr(kAccountKeyPrefix.length)); + gAccountsService._keepAccount(this); + this.prefBranch = Services.prefs.getBranch(kPrefAccountPrefix + aKey + "."); + + if (aName) { + this.name = aName; + this.prefBranch.setStringPref(kPrefAccountName, aName); + + this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN; + } else { + this.name = this.prefBranch.getStringPref(kPrefAccountName); + } + + let prplId = aPrplId; + if (prplId) { + this.prefBranch.setCharPref(kPrefAccountPrpl, prplId); + } else { + prplId = this.prefBranch.getCharPref(kPrefAccountPrpl); + } + + // Get the protocol plugin, or fallback to an UnknownProtocol instance. + this.protocol = IMServices.core.getProtocolById(prplId); + if (!this.protocol) { + this.protocol = new UnknownProtocol(prplId); + this._connectionErrorReason = Ci.imIAccount.ERROR_UNKNOWN_PRPL; + return; + } + + // Ensure the account is correctly stored in blist.sqlite. + IMServices.contacts.storeAccount(this.numericId, this.name, prplId); + + // Get the prplIAccount from the protocol plugin. + this.prplAccount = this.protocol.getAccount(this); + + // Send status change notifications to the account. + this.observedStatusInfo = null; // (To execute the setter). + + // If we have never finished the first connection attempt for this account, + // mark the account as having caused a crash. + if (this.firstConnectionState == Ci.imIAccount.FIRST_CONNECTION_PENDING) { + this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_CRASHED; + } + + Services.logins.initializationPromise.then(() => { + // If protocol is falsy remove() was called on this instance while waiting + // for the promise to resolve. Since the instance was disposed there is + // nothing to do. + if (!this.protocol) { + return; + } + + // Check for errors that should prevent connection attempts. + if (this._passwordRequired && !this.password) { + this._connectionErrorReason = Ci.imIAccount.ERROR_MISSING_PASSWORD; + } else if ( + this.firstConnectionState == Ci.imIAccount.FIRST_CONNECTION_CRASHED + ) { + this._connectionErrorReason = Ci.imIAccount.ERROR_CRASHED; + } + }); +} + +imAccount.prototype = { + __proto__: ClassInfo(["imIAccount", "prplIAccount"], "im account object"), + + name: "", + id: "", + numericId: 0, + protocol: null, + prplAccount: null, + connectionState: Ci.imIAccount.STATE_DISCONNECTED, + connectionStateMsg: "", + connectionErrorMessage: "", + _connectionErrorReason: Ci.prplIAccount.NO_ERROR, + get connectionErrorReason() { + if ( + this._connectionErrorReason != Ci.prplIAccount.NO_ERROR && + (this._connectionErrorReason != Ci.imIAccount.ERROR_MISSING_PASSWORD || + !this._password) + ) { + return this._connectionErrorReason; + } + return this.prplAccount.connectionErrorReason; + }, + + observe(aSubject, aTopic, aData) { + if (aTopic == "account-connect-progress") { + this.connectionStateMsg = aData; + } else if (aTopic == "account-connecting") { + if (this.prplAccount.connectionErrorReason != Ci.prplIAccount.NO_ERROR) { + delete this.connectionErrorMessage; + if (this.timeOfNextReconnect - Date.now() > 1000) { + // This is a manual reconnection, reset the auto-reconnect stuff + this.timeOfLastConnect = 0; + this._cancelReconnection(); + } + } + if (this.firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_OK) { + this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_PENDING; + } + this.connectionState = Ci.imIAccount.STATE_CONNECTING; + } else if (aTopic == "account-connected") { + this.connectionState = Ci.imIAccount.STATE_CONNECTED; + this._finishedAutoLogin(); + this.timeOfLastConnect = Date.now(); + if (this.firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_OK) { + this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_OK; + } + delete this.connectionStateMsg; + + if ( + this.canJoinChat && + this.prefBranch.prefHasUserValue(kPrefAccountAutoJoin) + ) { + let autojoin = this.prefBranch.getStringPref(kPrefAccountAutoJoin); + if (autojoin) { + for (let room of autojoin.trim().split(/,\s*/)) { + if (room) { + this.joinChat(this.getChatRoomDefaultFieldValues(room)); + } + } + } + } + } else if (aTopic == "account-disconnecting") { + this.connectionState = Ci.imIAccount.STATE_DISCONNECTING; + this.connectionErrorMessage = aData; + delete this.connectionStateMsg; + this._finishedAutoLogin(); + + let firstConnectionState = this.firstConnectionState; + if ( + firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_OK && + firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_CRASHED + ) { + this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN; + } + + let connectionErrorReason = this.prplAccount.connectionErrorReason; + if (connectionErrorReason != Ci.prplIAccount.NO_ERROR) { + if ( + connectionErrorReason == Ci.prplIAccount.ERROR_NETWORK_ERROR || + connectionErrorReason == Ci.prplIAccount.ERROR_ENCRYPTION_ERROR + ) { + this._startReconnectTimer(); + } + this._sendNotification("account-connect-error"); + } + } else if (aTopic == "account-disconnected") { + this.connectionState = Ci.imIAccount.STATE_DISCONNECTED; + let connectionErrorReason = this.prplAccount.connectionErrorReason; + if (connectionErrorReason != Ci.prplIAccount.NO_ERROR) { + // If the account was disconnected with an error, save the debug messages. + this._omittedDebugMessagesBeforeError += this._omittedDebugMessages; + if (this._debugMessagesBeforeError) { + this._omittedDebugMessagesBeforeError += + this._debugMessagesBeforeError.length; + } + this._debugMessagesBeforeError = this._debugMessages; + } else { + // After a clean disconnection, drop the debug messages that + // could have been left by a previous error. + delete this._omittedDebugMessagesBeforeError; + delete this._debugMessagesBeforeError; + } + delete this._omittedDebugMessages; + delete this._debugMessages; + if ( + this._statusObserver && + connectionErrorReason == Ci.prplIAccount.NO_ERROR && + this.statusInfo.statusType > Ci.imIStatusInfo.STATUS_OFFLINE + ) { + // If the status changed back to online while an account was still + // disconnecting, it was not reconnected automatically at that point, + // so we must do it now. (This happens for protocols like IRC where + // disconnection is not immediate.) + this._sendNotification(aTopic, aData); + this.connect(); + return; + } + } else { + throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); + } + this._sendNotification(aTopic, aData); + }, + + _debugMessages: null, + _omittedDebugMessages: 0, + _debugMessagesBeforeError: null, + _omittedDebugMessagesBeforeError: 0, + logDebugMessage(aMessage, aLevel) { + if (!this._debugMessages) { + this._debugMessages = []; + } + if ( + lazy._maxDebugMessages && + this._debugMessages.length >= lazy._maxDebugMessages + ) { + this._debugMessages.shift(); + ++this._omittedDebugMessages; + } + this._debugMessages.push({ logLevel: aLevel, message: aMessage }); + }, + _createDebugMessage(aMessage) { + let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance( + Ci.nsIScriptError + ); + scriptError.init( + aMessage, + "", + "", + 0, + null, + Ci.nsIScriptError.warningFlag, + "component javascript" + ); + return { logLevel: 0, message: scriptError }; + }, + getDebugMessages() { + let messages = []; + if (this._omittedDebugMessagesBeforeError) { + let text = this._omittedDebugMessagesBeforeError + " messages omitted"; + messages.push(this._createDebugMessage(text)); + } + if (this._debugMessagesBeforeError) { + messages = messages.concat(this._debugMessagesBeforeError); + } + if (this._omittedDebugMessages) { + let text = this._omittedDebugMessages + " messages omitted"; + messages.push(this._createDebugMessage(text)); + } + if (this._debugMessages) { + messages = messages.concat(this._debugMessages); + } + if (messages.length) { + let appInfo = Services.appinfo; + let header = + `${appInfo.name} ${appInfo.version} (${appInfo.appBuildID}), ` + + `Gecko ${appInfo.platformVersion} (${appInfo.platformBuildID}) ` + + `on ${lazy.HttpProtocolHandler.oscpu}`; + messages.unshift(this._createDebugMessage(header)); + } + + return messages; + }, + + _observedStatusInfo: null, + get observedStatusInfo() { + return this._observedStatusInfo; + }, + _statusObserver: null, + set observedStatusInfo(aUserStatusInfo) { + if (!this.prplAccount) { + return; + } + if (this._statusObserver) { + this.statusInfo.removeObserver(this._statusObserver); + } + this._observedStatusInfo = aUserStatusInfo; + if (this._statusObserver) { + this.statusInfo.addObserver(this._statusObserver); + } + }, + _removeStatusObserver() { + if (this._statusObserver) { + this.statusInfo.removeObserver(this._statusObserver); + delete this._statusObserver; + } + }, + get statusInfo() { + return this._observedStatusInfo || IMServices.core.globalUserStatus; + }, + + reconnectAttempt: 0, + timeOfLastConnect: 0, + timeOfNextReconnect: 0, + _reconnectTimer: null, + _startReconnectTimer() { + if (Services.io.offline) { + console.error("_startReconnectTimer called while offline"); + return; + } + + /* If the last successful connection is older than 10 seconds, reset the + number of reconnection attempts. */ + const kTimeBeforeSuccessfulConnection = 10; + if ( + this.timeOfLastConnect && + this.timeOfLastConnect + kTimeBeforeSuccessfulConnection * 1000 < + Date.now() + ) { + delete this.reconnectAttempt; + delete this.timeOfLastConnect; + } + + let timers = Services.prefs + .getCharPref("messenger.accounts.reconnectTimer") + .split(","); + let delay = timers[Math.min(this.reconnectAttempt, timers.length - 1)]; + let msDelay = parseInt(delay) * 1000; + ++this.reconnectAttempt; + this.timeOfNextReconnect = Date.now() + msDelay; + this._reconnectTimer = setTimeout(this.connect.bind(this), msDelay); + }, + + _sendNotification(aTopic, aData) { + Services.obs.notifyObservers(this, aTopic, aData); + }, + + get firstConnectionState() { + try { + return this.prefBranch.getIntPref(kPrefAccountFirstConnectionState); + } catch (e) { + return Ci.imIAccount.FIRST_CONNECTION_OK; + } + }, + set firstConnectionState(aState) { + if (aState == Ci.imIAccount.FIRST_CONNECTION_OK) { + this.prefBranch.clearUserPref(kPrefAccountFirstConnectionState); + } else { + this.prefBranch.setIntPref(kPrefAccountFirstConnectionState, aState); + // We want to save this pref immediately when trying to connect. + if (aState == Ci.imIAccount.FIRST_CONNECTION_PENDING) { + SavePrefTimer.saveNow(); + } else { + SavePrefTimer.initTimer(); + } + } + }, + + _pendingReconnectForConnectionInfoChange: false, + _connectionInfoChanged() { + // The next connection will be the first connection with these parameters. + this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN; + + // We want to attempt to reconnect with the new settings only if a + // previous attempt failed or a connection attempt is currently + // pending (so we can return early if the account is currently + // connected or disconnected without error). + // The code doing the reconnection attempt is wrapped within an + // executeSoon call so that when multiple settings are changed at + // once we don't attempt to reconnect until they are all saved. + // If a reconnect attempt is already scheduled, we can also return early. + if ( + this._pendingReconnectForConnectionInfoChange || + this.connected || + (this.disconnected && + this.connectionErrorReason == Ci.prplIAccount.NO_ERROR) + ) { + return; + } + + this._pendingReconnectForConnectionInfoChange = true; + executeSoon( + function () { + delete this._pendingReconnectForConnectionInfoChange; + // If the connection parameters have changed while we were + // trying to connect, cancel the ongoing connection attempt and + // try again with the new parameters. + if (this.connecting) { + this.disconnect(); + this.connect(); + return; + } + // If the account was disconnected because of a non-fatal + // connection error, retry now that we have new parameters. + let errorReason = this.connectionErrorReason; + if ( + this.disconnected && + errorReason != Ci.prplIAccount.NO_ERROR && + errorReason != Ci.imIAccount.ERROR_MISSING_PASSWORD && + errorReason != Ci.imIAccount.ERROR_CRASHED && + errorReason != Ci.imIAccount.ERROR_UNKNOWN_PRPL + ) { + this.connect(); + } + }.bind(this) + ); + }, + + // If the protocol plugin is missing, we can't access the normalizedName, + // but in lots of cases this.name is equivalent. + get normalizedName() { + return this.prplAccount ? this.prplAccount.normalizedName : this.name; + }, + normalize(aName) { + return this.prplAccount ? this.prplAccount.normalize(aName) : aName; + }, + + _sendUpdateNotification() { + this._sendNotification("account-updated"); + }, + + set alias(val) { + if (val) { + this.prefBranch.setStringPref(kPrefAccountAlias, val); + } else { + this.prefBranch.clearUserPref(kPrefAccountAlias); + } + this._sendUpdateNotification(); + }, + get alias() { + try { + return this.prefBranch.getStringPref(kPrefAccountAlias); + } catch (e) { + return ""; + } + }, + + _password: "", + get password() { + if (this._password) { + return this._password; + } + + // Avoid prompting the user for the primary password more than once at startup. + if (gUserCanceledPrimaryPasswordPrompt) { + return ""; + } + + let passwordURI = "im://" + this.protocol.id; + let logins; + try { + logins = Services.logins.findLogins(passwordURI, null, passwordURI); + } catch (e) { + this._handlePrimaryPasswordException(e); + return ""; + } + let normalizedName = this.normalizedName; + for (let login of logins) { + if (login.username == normalizedName) { + this._password = login.password; + if ( + this._connectionErrorReason == Ci.imIAccount.ERROR_MISSING_PASSWORD + ) { + // We have found a password for an account marked as missing password, + // re-check all others accounts missing a password. But first, + // remove the error on our own account to avoid re-checking it. + delete this._connectionErrorReason; + gAccountsService._checkIfPasswordStillMissing(); + } + return this._password; + } + } + return ""; + }, + _checkIfPasswordStillMissing() { + if ( + this._connectionErrorReason != Ci.imIAccount.ERROR_MISSING_PASSWORD || + !this.password + ) { + return; + } + + delete this._connectionErrorReason; + this._sendUpdateNotification(); + }, + get _passwordRequired() { + return !this.protocol.noPassword && !this.protocol.passwordOptional; + }, + set password(aPassword) { + this._password = aPassword; + if (gUserCanceledPrimaryPasswordPrompt) { + return; + } + let newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance( + Ci.nsILoginInfo + ); + let passwordURI = "im://" + this.protocol.id; + newLogin.init( + passwordURI, + null, + passwordURI, + this.normalizedName, + aPassword, + "", + "" + ); + try { + let logins = Services.logins.findLogins(passwordURI, null, passwordURI); + let saved = false; + for (let login of logins) { + if (newLogin.matches(login, true)) { + if (aPassword) { + Services.logins.modifyLogin(login, newLogin); + } else { + Services.logins.removeLogin(login); + } + saved = true; + break; + } + } + if (!saved && aPassword) { + Services.logins.addLogin(newLogin); + } + } catch (e) { + this._handlePrimaryPasswordException(e); + } + + this._connectionInfoChanged(); + if ( + aPassword && + this._connectionErrorReason == Ci.imIAccount.ERROR_MISSING_PASSWORD + ) { + this._connectionErrorReason = Ci.imIAccount.NO_ERROR; + } else if (!aPassword && this._passwordRequired) { + this._connectionErrorReason = Ci.imIAccount.ERROR_MISSING_PASSWORD; + } + this._sendUpdateNotification(); + }, + _handlePrimaryPasswordException(aException) { + if (aException.result != Cr.NS_ERROR_ABORT) { + throw aException; + } + + gUserCanceledPrimaryPasswordPrompt = true; + executeSoon(function () { + gUserCanceledPrimaryPasswordPrompt = false; + }); + }, + + get autoLogin() { + return this.prefBranch.getBoolPref(kPrefAccountAutoLogin, true); + }, + set autoLogin(val) { + this.prefBranch.setBoolPref(kPrefAccountAutoLogin, val); + SavePrefTimer.initTimer(); + this._sendUpdateNotification(); + }, + _autoLoginPending: false, + checkAutoLogin() { + // No auto-login if: the account has an error at the imIAccount level + // (unknown protocol, missing password, first connection crashed), + // the account is already connected or connecting, or autoLogin is off. + if ( + this._connectionErrorReason != Ci.prplIAccount.NO_ERROR || + this.connecting || + this.connected || + !this.autoLogin + ) { + return; + } + + this._autoLoginPending = true; + AutoLoginCounter.startAutoLogin(); + try { + this.connect(); + } catch (e) { + console.error(e); + this._finishedAutoLogin(); + } + }, + _finishedAutoLogin() { + if (!this.hasOwnProperty("_autoLoginPending")) { + return; + } + delete this._autoLoginPending; + AutoLoginCounter.finishedAutoLogin(); + }, + + // Delete the account (from the preferences, mozStorage, and call unInit). + remove() { + let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance( + Ci.nsILoginInfo + ); + let passwordURI = "im://" + this.protocol.id; + // Note: the normalizedName may not be exactly right if the + // protocol plugin is missing. + login.init(passwordURI, null, passwordURI, this.normalizedName, "", "", ""); + let logins = Services.logins.findLogins(passwordURI, null, passwordURI); + for (let l of logins) { + if (login.matches(l, true)) { + Services.logins.removeLogin(l); + break; + } + } + if (this.connected || this.connecting) { + this.disconnect(); + } + if (this.prplAccount) { + this.prplAccount.remove(); + } + this.unInit(); + IMServices.contacts.forgetAccount(this.numericId); + for (let prefName of this.prefBranch.getChildList("")) { + this.prefBranch.clearUserPref(prefName); + } + }, + unInit() { + // remove any pending reconnection timer. + this._cancelReconnection(); + + // Keeping a status observer could cause an immediate reconnection. + this._removeStatusObserver(); + + // remove any pending autologin preference used for crash detection. + this._finishedAutoLogin(); + + // If the first connection was pending on quit, we set it back to unknown. + if (this.firstConnectionState == Ci.imIAccount.FIRST_CONNECTION_PENDING) { + this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN; + } + + // and make sure we cleanup the save pref timer. + SavePrefTimer.unInitTimer(); + + if (this.prplAccount) { + this.prplAccount.unInit(); + } + + delete this.protocol; + delete this.prplAccount; + }, + + get _ensurePrplAccount() { + if (this.prplAccount) { + return this.prplAccount; + } + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + connect() { + if (!this.prplAccount) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + if (this._passwordRequired) { + // If the previous connection attempt failed because we have a wrong password, + // clear the passwor cache so that if there's no password in the password + // manager the user gets prompted again. + if ( + this.connectionErrorReason == + Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED + ) { + delete this._password; + } + + let password = this.password; + if (!password) { + let prompts = Services.prompt; + let shouldSave = { value: false }; + password = { value: "" }; + if ( + !prompts.promptPassword( + null, + lazy._("passwordPromptTitle", this.name), + lazy._("passwordPromptText", this.name), + password, + lazy._("passwordPromptSaveCheckbox"), + shouldSave + ) + ) { + return; + } + + if (shouldSave.value) { + this.password = password.value; + } else { + this._password = password.value; + } + } + } + + if (!this._statusObserver) { + this._statusObserver = { + observe: function (aSubject, aTopic, aData) { + // Disconnect or reconnect the account automatically, otherwise notify + // the prplAccount instance. + let statusType = aSubject.statusType; + let connectionErrorReason = this.connectionErrorReason; + if (statusType == Ci.imIStatusInfo.STATUS_OFFLINE) { + if (this.connected || this.connecting) { + this.prplAccount.disconnect(); + } + this._cancelReconnection(); + } else if ( + statusType > Ci.imIStatusInfo.STATUS_OFFLINE && + this.disconnected && + (connectionErrorReason == Ci.prplIAccount.NO_ERROR || + connectionErrorReason == Ci.prplIAccount.ERROR_NETWORK_ERROR || + connectionErrorReason == Ci.prplIAccount.ERROR_ENCRYPTION_ERROR) + ) { + this.prplAccount.connect(); + } else if (this.connected) { + this.prplAccount.observe(aSubject, aTopic, aData); + } + }.bind(this), + }; + + this.statusInfo.addObserver(this._statusObserver); + } + + if ( + !Services.io.offline && + this.statusInfo.statusType > Ci.imIStatusInfo.STATUS_OFFLINE && + this.disconnected + ) { + this.prplAccount.connect(); + } + }, + disconnect() { + this._removeStatusObserver(); + if (!this.disconnected) { + this._ensurePrplAccount.disconnect(); + } + }, + + get disconnected() { + return this.connectionState == Ci.imIAccount.STATE_DISCONNECTED; + }, + get connected() { + return this.connectionState == Ci.imIAccount.STATE_CONNECTED; + }, + get connecting() { + return this.connectionState == Ci.imIAccount.STATE_CONNECTING; + }, + get disconnecting() { + return this.connectionState == Ci.imIAccount.STATE_DISCONNECTING; + }, + + _cancelReconnection() { + if (this._reconnectTimer) { + clearTimeout(this._reconnectTimer); + delete this._reconnectTimer; + } + delete this.reconnectAttempt; + delete this.timeOfNextReconnect; + }, + cancelReconnection() { + if (!this.disconnected) { + throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); + } + + // Ensure we don't keep a status observer that could re-enable the + // auto-reconnect timers. + this.disconnect(); + + this._cancelReconnection(); + }, + createConversation(aName) { + return this._ensurePrplAccount.createConversation(aName); + }, + addBuddy(aTag, aName) { + this._ensurePrplAccount.addBuddy(aTag, aName); + }, + loadBuddy(aBuddy, aTag) { + if (this.prplAccount) { + return this.prplAccount.loadBuddy(aBuddy, aTag); + } + // Generate dummy account buddies for unknown protocols. + return new UnknownAccountBuddy(this, aBuddy, aTag); + }, + requestBuddyInfo(aBuddyName) { + this._ensurePrplAccount.requestBuddyInfo(aBuddyName); + }, + getChatRoomFields() { + return this._ensurePrplAccount.getChatRoomFields(); + }, + getChatRoomDefaultFieldValues(aDefaultChatName) { + return this._ensurePrplAccount.getChatRoomDefaultFieldValues( + aDefaultChatName + ); + }, + get canJoinChat() { + return this.prplAccount ? this.prplAccount.canJoinChat : false; + }, + joinChat(aComponents) { + this._ensurePrplAccount.joinChat(aComponents); + }, + setBool(aName, aVal) { + this.prefBranch.setBoolPref(kAccountOptionPrefPrefix + aName, aVal); + this._connectionInfoChanged(); + if (this.prplAccount) { + this.prplAccount.setBool(aName, aVal); + } + SavePrefTimer.initTimer(); + }, + setInt(aName, aVal) { + this.prefBranch.setIntPref(kAccountOptionPrefPrefix + aName, aVal); + this._connectionInfoChanged(); + if (this.prplAccount) { + this.prplAccount.setInt(aName, aVal); + } + SavePrefTimer.initTimer(); + }, + setString(aName, aVal) { + this.prefBranch.setStringPref(kAccountOptionPrefPrefix + aName, aVal); + this._connectionInfoChanged(); + if (this.prplAccount) { + this.prplAccount.setString(aName, aVal); + } + SavePrefTimer.initTimer(); + }, + save() { + SavePrefTimer.saveNow(); + }, + + getSessions() { + return this._ensurePrplAccount.getSessions(); + }, + get encryptionStatus() { + return this._ensurePrplAccount.encryptionStatus; + }, +}; + +var gAccountsService = null; + +export function AccountsService() {} +AccountsService.prototype = { + initAccounts() { + this._initAutoLoginStatus(); + this._accounts = []; + this._accountsById = {}; + gAccountsService = this; + let accountIdArray = MailServices.accounts.accounts + .map(account => account.incomingServer.getCharValue("imAccount")) + .filter(accountKey => accountKey?.startsWith(kAccountKeyPrefix)); + for (let account of accountIdArray) { + new imAccount(account); + } + + this._prefObserver = this.observe.bind(this); + Services.prefs.addObserver(kPrefAccountOrder, this._prefObserver); + }, + + _prefObserver: null, + observe(aSubject, aTopic, aData) { + if (aTopic != "nsPref:changed" || aData != kPrefAccountOrder) { + return; + } + + const imAccounts = MailServices.accounts.accounts + .map(account => account.incomingServer.getCharValue("imAccount")) + .filter(k => k?.startsWith(kAccountKeyPrefix)) + .map(k => + this.getAccountByNumericId(parseInt(k.substr(kAccountKeyPrefix.length))) + ) + .filter(a => a); + + // Only update _accounts if it's a reorder operation + if (imAccounts.length == this._accounts.length) { + this._accounts = imAccounts; + Services.obs.notifyObservers(this, "account-list-updated"); + } + }, + + unInitAccounts() { + for (let account of this._accounts) { + account.unInit(); + } + gAccountsService = null; + delete this._accounts; + delete this._accountsById; + Services.prefs.removeObserver(kPrefAccountOrder, this._prefObserver); + delete this._prefObserver; + }, + + autoLoginStatus: Ci.imIAccountsService.AUTOLOGIN_ENABLED, + _initAutoLoginStatus() { + /* If auto-login is already disabled, do nothing */ + if (this.autoLoginStatus != Ci.imIAccountsService.AUTOLOGIN_ENABLED) { + return; + } + + let prefs = Services.prefs; + if (!prefs.getIntPref("messenger.startup.action")) { + // the value 0 means that we start without connecting the accounts + this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_USER_DISABLED; + return; + } + + /* Disable auto-login if we are running in safe mode */ + if (Services.appinfo.inSafeMode) { + this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_SAFE_MODE; + return; + } + + /* Check if we crashed at the last startup during autologin */ + let autoLoginPending; + if ( + prefs.getPrefType(kPrefAutologinPending) == prefs.PREF_INVALID || + !(autoLoginPending = prefs.getIntPref(kPrefAutologinPending)) + ) { + // if the pref isn't set, then we haven't crashed: keep autologin enabled + return; + } + + // Last autologin hasn't finished properly. + // For now, assume it's because of a crash. + this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_CRASH; + prefs.deleteBranch(kPrefAutologinPending); + + // If the crash reporter isn't built, we can't know anything more. + if (!("nsICrashReporter" in Ci)) { + return; + } + + try { + // Try to get more info with breakpad + let lastCrashTime = 0; + + /* Locate the LastCrash file */ + let lastCrash = Services.dirsvc.get("UAppData", Ci.nsIFile); + lastCrash.append("Crash Reports"); + lastCrash.append("LastCrash"); + if (lastCrash.exists()) { + /* Ok, the file exists, now let's try to read it */ + let is = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + is.init(lastCrash, -1, 0, 0); + sis.init(sis); + + lastCrashTime = parseInt(sis.read(lastCrash.fileSize)); + + sis.close(); + } + // The file not existing is totally acceptable, it just means that + // either we never crashed or breakpad is not enabled. + // In this case, lastCrashTime will keep its 0 initialization value. + + /* dump("autoLoginPending = " + autoLoginPending + + ", lastCrash = " + lastCrashTime + + ", difference = " + lastCrashTime - autoLoginPending + "\n");*/ + + if (lastCrashTime < autoLoginPending) { + // the last crash caught by breakpad is older than our last autologin + // attempt. + // If breakpad is currently enabled, we can be confident that + // autologin was interrupted for an exterior reason + // (application killed by the user, power outage, ...) + try { + Services.appinfo + .QueryInterface(Ci.nsICrashReporter) + .annotateCrashReport("=", ""); + } catch (e) { + // This should fail with NS_ERROR_INVALID_ARG if breakpad is enabled, + // and NS_ERROR_NOT_INITIALIZED if it is not. + if (e.result != Cr.NS_ERROR_NOT_INITIALIZED) { + this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_ENABLED; + } + } + } + } catch (e) { + // if we failed to get the last crash time, then keep the + // AUTOLOGIN_CRASH value in mAutoLoginStatus and return. + } + }, + + processAutoLogin() { + if (!this._accounts) { + // if we're already shutting down + return; + } + + for (let account of this._accounts) { + account.checkAutoLogin(); + } + + // Make sure autologin is now enabled, so that we don't display a + // message stating that it is disabled and asking the user if it + // should be processed now. + this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_ENABLED; + + // Notify observers so that any message stating that autologin is + // disabled can be removed + Services.obs.notifyObservers(this, "autologin-processed"); + }, + + _checkingIfPasswordStillMissing: false, + _checkIfPasswordStillMissing() { + // Avoid recursion. + if (this._checkingIfPasswordStillMissing) { + return; + } + + this._checkingIfPasswordStillMissing = true; + for (let account of this._accounts) { + account._checkIfPasswordStillMissing(); + } + delete this._checkingIfPasswordStillMissing; + }, + + getAccountById(aAccountId) { + if (!aAccountId.startsWith(kAccountKeyPrefix)) { + throw Components.Exception( + `Invalid id: ${aAccountId}`, + Cr.NS_ERROR_INVALID_ARG + ); + } + + let id = parseInt(aAccountId.substr(kAccountKeyPrefix.length)); + return this.getAccountByNumericId(id); + }, + + _keepAccount(aAccount) { + this._accounts.push(aAccount); + this._accountsById[aAccount.numericId] = aAccount; + }, + getAccountByNumericId(aAccountId) { + return this._accountsById[aAccountId]; + }, + getAccounts() { + return this._accounts; + }, + + createAccount(aName, aPrpl) { + // Ensure an account with the same name and protocol doesn't already exist. + let prpl = IMServices.core.getProtocolById(aPrpl); + if (!prpl) { + throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); + } + if (prpl.accountExists(aName)) { + console.error("Attempted to create a duplicate account!"); + throw Components.Exception("", Cr.NS_ERROR_ALREADY_INITIALIZED); + } + + /* First get a unique id for the new account. */ + let id; + for (id = 1; ; ++id) { + if (this._accountsById.hasOwnProperty(id)) { + continue; + } + + /* id isn't used by a known account, double check it isn't + already used in the sqlite database. This should never + happen, except if we have a corrupted profile. */ + if (!IMServices.contacts.accountIdExists(id)) { + break; + } + Services.console.logStringMessage( + "No account " + + id + + " but there is some data in the buddy list for an account with this number. Your profile may be corrupted." + ); + } + + /* Actually create the new account. */ + let key = kAccountKeyPrefix + id; + let account = new imAccount(key, aName, aPrpl); + + Services.obs.notifyObservers(account, "account-added"); + return account; + }, + + deleteAccount(aAccountId) { + let account = this.getAccountById(aAccountId); + if (!account) { + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + let index = this._accounts.indexOf(account); + if (index == -1) { + throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); + } + + let id = account.numericId; + account.remove(); + this._accounts.splice(index, 1); + delete this._accountsById[id]; + Services.obs.notifyObservers(account, "account-removed"); + }, + + QueryInterface: ChromeUtils.generateQI(["imIAccountsService"]), + classDescription: "Accounts", +}; |