# HG changeset patch # User souliane # Date 1393837215 -3600 # Node ID 52ee240acc9c679b5f6e7a7c9f7012dcb0513382 # Parent 57c32d8ec847062fe24e0bf1cc1e21e1d7914095 plugin account: user can change his password or delete his XMPP account diff -r 57c32d8ec847 -r 52ee240acc9c src/plugins/plugin_misc_account.py --- a/src/plugins/plugin_misc_account.py Mon Mar 03 15:37:49 2014 +0100 +++ b/src/plugins/plugin_misc_account.py Mon Mar 03 10:00:15 2014 +0100 @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sat.core.i18n import _ +from sat.core.i18n import _, D_ from logging import debug, info, warning, error from sat.core import exceptions from twisted.internet import reactor, defer, protocol @@ -26,6 +26,7 @@ from twisted.python.failure import Failure from email.mime.text import MIMEText from twisted.mail.smtp import sendmail +from sat.tools import xml_tools PLUGIN_INFO = { "name": "Account Plugin", @@ -56,16 +57,26 @@ } +class PasswordsMatchingError(Exception): + pass + + class ProsodyRegisterProtocol(protocol.ProcessProtocol): """ Try to register an account with prosody """ - def __init__(self, password, deferred=None): + def __init__(self, password=None, deferred=None): + """ + @param password: new user password + @param deferred + """ self.password = password self.deferred = deferred self.data = '' def connectionMade(self): - self.transport.write("%s\n%s" % ((self.password.encode('utf-8'), ) * 2)) + if self.password is None: + return + self.transport.write("%s\n%s" % ((self.password.encode('utf-8'),) * 2)) self.transport.closeStdin() def outReceived(self, data): @@ -76,12 +87,27 @@ def processEnded(self, reason): if (reason.value.exitCode == 0): - info(_('Prosody registration success')) + info(_('Prosody command succeed')) self.deferred.callback(None) else: - error(_(u"Can't register Prosody account (error code: %(code)d): %(message)s") % {'code': reason.value.exitCode, 'message': self.data}) + error(_(u"Can't complete Prosody command (error code: %(code)d): %(message)s") % {'code': reason.value.exitCode, 'message': self.data}) self.deferred.errback(Failure(exceptions.InternalError)) + @classmethod + def prosodyctl(cls, plugin, command, password=None, profile=None): + """Create a new ProsodyRegisterProtocol and execute the given prosodyctl command. + @param plugin: instance of MiscAccount + @param command: the command to execute: "adduser", "passwd" or "deluser" + @param password: the user new password (leave to None for "deluser" command) + @param profile: the user profile + @return a Deferred instance + """ + d = defer.Deferred() + prosody_reg = ProsodyRegisterProtocol(password, d) + prosody_exe = join(plugin._prosody_path, plugin.getConfig('prosodyctl')) + reactor.spawnProcess(prosody_reg, prosody_exe, [prosody_exe, command, "%s@%s" % (profile, plugin.getConfig('new_account_domain'))], path=plugin._prosody_path) + return d + class MiscAccount(object): """Account plugin: create a SàT + Prosody account, used by Libervia""" @@ -93,6 +119,7 @@ self.host = host host.bridge.addMethod("registerSatAccount", ".plugin", in_sign='sss', out_sign='', method=self._registerAccount, async=True) host.bridge.addMethod("getNewAccountDomain", ".plugin", in_sign='', out_sign='s', method=self._getNewAccountDomain, async=False) + host.bridge.addMethod("getAccountDialogUI", ".plugin", in_sign='s', out_sign='s', method=self._getAccountDialogUI, async=False) self._prosody_path = self.getConfig('prosody_path') if self._prosody_path is None: paths = which(self.getConfig('prosodyctl')) @@ -102,6 +129,9 @@ self._prosody_path = dirname(paths[0]) info(_('Prosody path found: %s') % (self._prosody_path, )) + self.__account_cb_id = host.registerCallback(self._accountDialogCb, with_data=True) + self.__delete_account_id = host.registerCallback(self.__deleteAccountCb, with_data=True) + def getConfig(self, name): return self.host.memory.getConfig(CONFIG_SECTION, name) or default_conf[name] @@ -138,11 +168,7 @@ #XXX: we use "prosodyctl adduser" because "register" doesn't check conflict # and just change the password if the account already exists - d = defer.Deferred() - prosody_reg = ProsodyRegisterProtocol(password, d) - prosody_exe = join(self._prosody_path, self.getConfig('prosodyctl')) - reactor.spawnProcess(prosody_reg, prosody_exe, [prosody_exe, 'adduser', "%s@%s" % (profile, self.getConfig('new_account_domain'))], path=self._prosody_path) - + d = ProsodyRegisterProtocol.prosodyctl(self, 'adduser', password, profile) d.addCallback(self._sendEmails, profile, email, password) d.addCallback(lambda ignore: None) return d @@ -204,3 +230,103 @@ """@return: the domain that will be set to new account""" return self.getConfig('new_account_domain') + def _getAccountDialogUI(self, profile): + """Get the main dialog to manage your account + @param menu_data + @param profile: %(doc_profile)s + @return XML of the dialog + """ + form_ui = xml_tools.XMLUI("form", "tabs", title=D_("Manage your XMPP account"), submit_id=self.__account_cb_id) + tab_container = form_ui.current_container + tab_container.addTab("update", D_("Change your password"), container=xml_tools.PairsContainer) + form_ui.addLabel(D_("Current password")) + form_ui.addPassword("current_passwd", value="") + form_ui.addLabel(D_("New password")) + form_ui.addPassword("new_passwd1", value="") + form_ui.addLabel(D_("New password (again)")) + form_ui.addPassword("new_passwd2", value="") + tab_container.addTab("delete", D_("Delete your account"), container=xml_tools.PairsContainer) + form_ui.addLabel(D_("Current password")) + form_ui.addPassword("delete_passwd", value="") + form_ui.addLabel(D_("Delete your account")) + form_ui.addBool("delete_checkbox", "false") + return form_ui.toXml() + + def _accountDialogCb(self, data, profile): + """Called when the user submits the main account dialog + @param data + @param profile + """ + password = self.host.memory.getParamA("Password", "Connection", profile_key=profile) + + def error_ui(): + error_ui = xml_tools.XMLUI("popup", title="Error") + error_ui.addText(D_("Passwords don't match!")) + return defer.succeed({'xmlui': error_ui.toXml()}) + + # check for account deletion + delete_passwd = data[xml_tools.SAT_FORM_PREFIX + 'delete_passwd'] + delete_checkbox = data[xml_tools.SAT_FORM_PREFIX + 'delete_checkbox'] + if delete_checkbox == 'true': + if password == delete_passwd: + return self.__deleteAccount(profile) + return error_ui() + + # check for password modification + current_passwd = data[xml_tools.SAT_FORM_PREFIX + 'current_passwd'] + new_passwd1 = data[xml_tools.SAT_FORM_PREFIX + 'new_passwd1'] + new_passwd2 = data[xml_tools.SAT_FORM_PREFIX + 'new_passwd2'] + if new_passwd1 or new_passwd2: + if password == current_passwd and new_passwd1 == new_passwd2: + return self.__changePassword(new_passwd1, profile=profile) + return error_ui() + + return defer.succeed({}) + + def __changePassword(self, password, profile): + """Actually change the user XMPP account and SàT profile password + @param password: new password + @profile + """ + def passwordChanged(result): + self.host.memory.setParam("Password", password, "Connection", profile_key=profile) + confirm_ui = xml_tools.XMLUI("popup", title="Confirmation") + confirm_ui.addText(D_("Your password has been changed.")) + return defer.succeed({'xmlui': confirm_ui.toXml()}) + + def errback(failure): + error_ui = xml_tools.XMLUI("popup", title="Error") + error_ui.addText(D_("Your password could not be changed: %s") % failure.getErrorMessage()) + return defer.succeed({'xmlui': error_ui.toXml()}) + + d = ProsodyRegisterProtocol.prosodyctl(self, 'passwd', password, profile=profile) + d.addCallbacks(passwordChanged, errback) + return d + + def __deleteAccount(self, profile): + """Ask for a confirmation before deleting the XMPP account and SàT profile + @param profile + """ + form_ui = xml_tools.XMLUI("form", title=D_("Delete your account ?"), submit_id=self.__delete_account_id) + form_ui.addText(D_("If you confirm this dialog, you will be disconnected and then your XMPP account AND your SàT profile will both be DELETED.")) + form_ui.addText(D_("There is no other confirmation dialog, this is the very last one! Are you sure?")) + return {'xmlui': form_ui.toXml()} + + def __deleteAccountCb(self, data, profile): + """Actually delete the XMPP account and SàT profile + @param data + @param profile + """ + def userDeleted(result): + self.host.disconnect(profile) + self.host.memory.asyncDeleteProfile(profile, force=True) + return defer.succeed({}) + + def errback(failure): + error_ui = xml_tools.XMLUI("popup", title="Error") + error_ui.addText(D_("Your XMPP account could not be deleted: %s") % failure.getErrorMessage()) + return defer.succeed({'xmlui': error_ui.toXml()}) + + d = ProsodyRegisterProtocol.prosodyctl(self, 'deluser', profile=profile) + d.addCallbacks(userDeleted, errback) + return d