diff src/plugins/plugin_misc_account.py @ 895:52ee240acc9c

plugin account: user can change his password or delete his XMPP account
author souliane <souliane@mailoo.org>
date Mon, 03 Mar 2014 10:00:15 +0100
parents e030460e065e
children 34dd9287dfe5
line wrap: on
line diff
--- 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 <http://www.gnu.org/licenses/>.
 
-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