Refactoring en cours.
[Plinn.git] / RegistrationTool.py
index 77f8bdc..1417f30 100644 (file)
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 #######################################################################################
 #   Plinn - http://plinn.org                                                          #
-#   Copyright (C) 2005-2007  Benoît PIN <benoit.pin@ensmp.fr>                         #
+#   © 2005-2013  Benoît PIN <pin@cri.ensmp.fr>                                        #
 #                                                                                     #
 #   This program is free software; you can redistribute it and/or                     #
 #   modify it under the terms of the GNU General Public License                       #
@@ -17,8 +17,8 @@
 #   along with this program; if not, write to the Free Software                       #
 #   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.   #
 #######################################################################################
-""" Plinn registration tool: implements 3 modes to register members :
-       anonymous, manager, reviewed.
+""" Plinn registration tool: implements 3 modes to register members:
+    anonymous, manager, reviewed.
 
 
 
@@ -29,11 +29,19 @@ from Products.PageTemplates.PageTemplateFile import PageTemplateFile
 from Products.CMFDefault.RegistrationTool import RegistrationTool as BaseRegistrationTool
 from AccessControl import ClassSecurityInfo, ModuleSecurityInfo
 from AccessControl.Permission import Permission
+from BTrees.OOBTree import OOBTree
 from Products.CMFCore.permissions import ManagePortal, AddPortalMember
 from Products.CMFCore.exceptions import AccessControl_Unauthorized
 from Products.CMFCore.utils import getToolByName
+from Products.CMFCore.utils import getUtilityByInterfaceName
 from Products.GroupUserFolder.GroupsToolPermissions import ManageGroups
+from Products.Plinn.utils import Message as _
+from Products.Plinn.utils import translate
+from Products.Plinn.utils import encodeQuopriEmail
+from Products.Plinn.utils import encodeMailHeader
+from DateTime import DateTime
 from types import TupleType, ListType
+from uuid import uuid4
 
 security = ModuleSecurityInfo('Products.Plinn.RegistrationTool')
 MODE_ANONYMOUS = 'anonymous'
@@ -55,133 +63,205 @@ security.declarePublic('DEFAULT_MEMBER_GROUP')
 
 class RegistrationTool(BaseRegistrationTool) :
 
-       """ Create and modify users by making calls to portal_membership.
-       """
-       
-       meta_type = "Plinn Registration Tool"
-       
-       manage_options = ({'label' : 'Registration mode', 'action' : 'manage_regmode'}, ) + \
-                                               BaseRegistrationTool.manage_options
-       
-       security = ClassSecurityInfo()
-       
-       security.declareProtected( ManagePortal, 'manage_regmode' )
-       manage_regmode = PageTemplateFile('www/configureRegistrationTool', globals(),
-                                                                               __name__='manage_regmode')
-
-       def __init__(self) :
-               self._mode = MODE_ANONYMOUS
-               self._chain = ''
-       
-       security.declareProtected(ManagePortal, 'configureTool')
-       def configureTool(self, registration_mode, chain, REQUEST=None) :
-               """ """
-               
-               if registration_mode not in MODES :
-                       raise ValueError, "Unknown mode: " + registration_mode
-               else :
-                       self._mode = registration_mode
-                       self._updatePortalRoleMappingForMode(registration_mode)
-               
-               wtool = getToolByName(self, 'portal_workflow')
-
-               if registration_mode == MODE_REVIEWED :
-                       if not hasattr(wtool, '_chains_by_type') :
-                               wtool._chains_by_type = PersistentMapping()
-                       wfids = []
-                       chain = chain.strip()
-                       
-                       if chain == '(Default)' :
-                               try : del wtool._chains_by_type['Member Data']
-                               except KeyError : pass
-                               self._chain = chain
-                       else :
-                               for wfid in chain.replace(',', ' ').split(' ') :
-                                       if wfid :
-                                               if not wtool.getWorkflowById(wfid) :
-                                                       raise ValueError, '"%s" is not a workflow ID.' % wfid
-                                               wfids.append(wfid)
-       
-                               wtool._chains_by_type['Member Data'] = tuple(wfids)
-                               self._chain = ', '.join(wfids)
-               else :
-                       wtool._chains_by_type['Member Data'] = tuple()
-               
-               if REQUEST :
-                       REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_regmode?manage_tabs_message=Saved changes.')
-
-       def _updatePortalRoleMappingForMode(self, mode) :
-       
-               urlTool = getToolByName(self, 'portal_url')
-               portal = urlTool.getPortalObject()
-       
-               if mode in [MODE_ANONYMOUS, MODE_REVIEWED] :
-                       portal.manage_permission(AddPortalMember, roles = ['Anonymous', 'Manager'], acquire=1)
-               elif mode == MODE_MANAGER :
-                       portal.manage_permission(AddPortalMember, roles = ['Manager', 'UserManager'], acquire=0)
-       
-       security.declarePublic('getMode')
-       def getMode(self) :
-               # """ return current mode """
-               return self._mode[:]
-       
-       security.declarePublic('getWfId')
-       def getWfChain(self) :
-               # """ return current workflow id """
-               return self._chain
-       
-       security.declarePublic('roleMappingMismatch')
-       def roleMappingMismatch(self) :
-               # """ test if the role mapping is correct for the currrent mode """
-               
-               mode = self._mode
-               urlTool = getToolByName(self, 'portal_url')
-               portal = urlTool.getPortalObject()
-                               
-               def rolesOfAddPortalMemberPerm() :
-                       p=Permission(AddPortalMember, [], portal)
-                       return p.getRoles()
-               
-               if mode in [MODE_ANONYMOUS, MODE_REVIEWED] :
-                       if 'Anonymous' in rolesOfAddPortalMemberPerm() : return False
-                       
-               elif mode == MODE_MANAGER :
-                       roles = rolesOfAddPortalMemberPerm()
-                       if 'Manager' in roles or 'UserManager' in roles and len(roles) == 1 and type(roles) == TupleType :
-                               return False
-               
-               return True
-
-       security.declareProtected(AddPortalMember, 'addMember')
-       def addMember(self, id, password, roles=(), groups=(DEFAULT_MEMBER_GROUP,), domains='', properties=None) :
-               """ Idem CMFCore but without default role """
-               BaseRegistrationTool.addMember(self, id, password, roles=roles,
-                                                                          domains=domains, properties=properties)
-
-               if self.getMode() in [MODE_ANONYMOUS, MODE_MANAGER] :
-                       gtool = getToolByName(self, 'portal_groups')
-                       mtool = getToolByName(self, 'portal_membership')
-                       utool = getToolByName(self, 'portal_url')
-                       portal = utool.getPortalObject()
-                       isGrpManager = mtool.checkPermission(ManageGroups, portal) ## TODO : CMF2.1 compat
-                       aclu = self.aq_inner.acl_users
-
-                       for gid in groups:
-                               g = gtool.getGroupById(gid)
-                               if not isGrpManager :                           
-                                       if gid != DEFAULT_MEMBER_GROUP:
-                                               raise AccessControl_Unauthorized, 'You are not allowed to join arbitrary group.'
-
-                               if g is None :
-                                       gtool.addGroup(gid)
-                               aclu.changeUser(aclu.getGroupPrefix() +gid, roles=['Member', ])
-                               g = gtool.getGroupById(gid)
-                               g.addMember(id)
-
-
-       def afterAdd(self, member, id, password, properties):
-               """ notify member creation """
-               member.notifyWorkflowCreated()
-               member.indexObject()
-               
+    """ Create and modify users by making calls to portal_membership.
+    """
+    
+    meta_type = "Plinn Registration Tool"
+    
+    manage_options = ({'label' : 'Registration mode', 'action' : 'manage_regmode'}, ) + \
+                        BaseRegistrationTool.manage_options
+    
+    security = ClassSecurityInfo()
+    
+    security.declareProtected( ManagePortal, 'manage_regmode' )
+    manage_regmode = PageTemplateFile('www/configureRegistrationTool', globals(),
+                                        __name__='manage_regmode')
+
+    def __init__(self) :
+        self._mode = MODE_ANONYMOUS
+        self._chain = ''
+        self._passwordResetRequests = OOBTree()
+    
+    security.declareProtected(ManagePortal, 'configureTool')
+    def configureTool(self, registration_mode, chain, REQUEST=None) :
+        """ """
+        
+        if registration_mode not in MODES :
+            raise ValueError, "Unknown mode: " + registration_mode
+        else :
+            self._mode = registration_mode
+            self._updatePortalRoleMappingForMode(registration_mode)
+        
+        wtool = getToolByName(self, 'portal_workflow')
+
+        if registration_mode == MODE_REVIEWED :
+            if not hasattr(wtool, '_chains_by_type') :
+                wtool._chains_by_type = PersistentMapping()
+            wfids = []
+            chain = chain.strip()
+            
+            if chain == '(Default)' :
+                try : del wtool._chains_by_type['Member Data']
+                except KeyError : pass
+                self._chain = chain
+            else :
+                for wfid in chain.replace(',', ' ').split(' ') :
+                    if wfid :
+                        if not wtool.getWorkflowById(wfid) :
+                            raise ValueError, '"%s" is not a workflow ID.' % wfid
+                        wfids.append(wfid)
+    
+                wtool._chains_by_type['Member Data'] = tuple(wfids)
+                self._chain = ', '.join(wfids)
+        else :
+            wtool._chains_by_type['Member Data'] = tuple()
+        
+        if REQUEST :
+            REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_regmode?manage_tabs_message=Saved changes.')
+
+    def _updatePortalRoleMappingForMode(self, mode) :
+    
+        urlTool = getToolByName(self, 'portal_url')
+        portal = urlTool.getPortalObject()
+    
+        if mode in [MODE_ANONYMOUS, MODE_REVIEWED] :
+            portal.manage_permission(AddPortalMember, roles = ['Anonymous', 'Manager'], acquire=1)
+        elif mode == MODE_MANAGER :
+            portal.manage_permission(AddPortalMember, roles = ['Manager', 'UserManager'], acquire=0)
+    
+    security.declarePublic('getMode')
+    def getMode(self) :
+        # """ return current mode """
+        return self._mode[:]
+    
+    security.declarePublic('getWfId')
+    def getWfChain(self) :
+        # """ return current workflow id """
+        return self._chain
+    
+    security.declarePublic('roleMappingMismatch')
+    def roleMappingMismatch(self) :
+        # """ test if the role mapping is correct for the currrent mode """
+        
+        mode = self._mode
+        urlTool = getToolByName(self, 'portal_url')
+        portal = urlTool.getPortalObject()
+                
+        def rolesOfAddPortalMemberPerm() :
+            p=Permission(AddPortalMember, [], portal)
+            return p.getRoles()
+        
+        if mode in [MODE_ANONYMOUS, MODE_REVIEWED] :
+            if 'Anonymous' in rolesOfAddPortalMemberPerm() : return False
+            
+        elif mode == MODE_MANAGER :
+            roles = rolesOfAddPortalMemberPerm()
+            if 'Manager' in roles or 'UserManager' in roles and len(roles) == 1 and type(roles) == TupleType :
+                return False
+        
+        return True
+
+    security.declareProtected(AddPortalMember, 'addMember')
+    def addMember(self, id, password, roles=(), groups=(DEFAULT_MEMBER_GROUP,), domains='', properties=None) :
+        """ Idem CMFCore but without default role """
+        BaseRegistrationTool.addMember(self, id, password, roles=roles,
+                                       domains=domains, properties=properties)
+
+        if self.getMode() in [MODE_ANONYMOUS, MODE_MANAGER] :
+            gtool = getToolByName(self, 'portal_groups')
+            mtool = getToolByName(self, 'portal_membership')
+            utool = getToolByName(self, 'portal_url')
+            portal = utool.getPortalObject()
+            isGrpManager = mtool.checkPermission(ManageGroups, portal) ## TODO : CMF2.1 compat
+            aclu = self.aq_inner.acl_users
+
+            for gid in groups:
+                g = gtool.getGroupById(gid)
+                if not isGrpManager :               
+                    if gid != DEFAULT_MEMBER_GROUP:
+                        raise AccessControl_Unauthorized, 'You are not allowed to join arbitrary group.'
+
+                if g is None :
+                    gtool.addGroup(gid)
+                aclu.changeUser(aclu.getGroupPrefix() +gid, roles=['Member', ])
+                g = gtool.getGroupById(gid)
+                g.addMember(id)
+
+
+    def afterAdd(self, member, id, password, properties):
+        """ notify member creation """
+        member.notifyWorkflowCreated()
+        member.indexObject()
+    
+
+    security.declarePublic('requestPasswordReset')
+    def requestPasswordReset(self, userid):
+        """ add uuid / (userid, expiration) pair and return uuid """
+        self.clearExpiredPasswordResetRequests()
+        mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
+        member = mtool.getMemberById(userid)
+        if member :
+            uuid = str(uuid4())
+            while self._passwordResetRequests.has_key(uuid) :
+                uuid = str(uuid4())
+            self._passwordResetRequests[uuid] = (userid, DateTime() + 1)
+            utool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IURLTool')
+            ptool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IPropertiesTool')
+            # fuck : mailhost récupéré avec getUtilityByInterfaceName n'est pas correctement
+            # wrappé. Un « unrestrictedTraverse » ne marche pas.
+            # mailhost = getUtilityByInterfaceName('Products.MailHost.interfaces.IMailHost')
+            portal = utool.getPortalObject()
+            mailhost = portal.MailHost
+            sender = encodeQuopriEmail(ptool.getProperty('email_from_name'), ptool.getProperty('email_from_address'))
+            to = encodeQuopriEmail(member.getMemberFullName(nameBefore=0), member.getProperty('email'))
+            subject = translate(_('How to reset your password on the %s website')) % ptool.getProperty('title')
+            subject = encodeMailHeader(subject)
+            options = {'fullName' : member.getMemberFullName(nameBefore=0),
+                       'siteName' : ptool.getProperty('title'),
+                       'resetPasswordUrl' : '%s/password_reset_form/%s' % (utool(), uuid)}
+            body = self.password_reset_mail(options)
+            message = self.echange_mail_template(From=sender,
+                                                 To=to,
+                                                 Subject=subject,
+                                                 ContentType = 'text/plain',
+                                                 charset = 'UTF-8',
+                                                 body=body)
+            mailhost.send(message)
+            return
+        
+        return _('Unknown user name. Please retry.')
+    
+    security.declarePrivate('clearExpiredPasswordResetRequests')
+    def clearExpiredPasswordResetRequests(self):
+        now = DateTime()
+        for uuid, record in self._passwordResetRequests.items() :
+            userid, date = record
+            if date < now :
+                del self._passwordResetRequests[uuid]
+    
+    
+    security.declarePublic('resetPassword')
+    def resetPassword(self, uuid, password, confirm) :
+        record = self._passwordResetRequests.get(uuid)
+        if not record :
+            return None, _('Invalid reset password request.')
+        
+        userid, expiration = record
+        now = DateTime()
+        if expiration < now :
+            self.clearExpiredPasswordResetRequests()
+            return None, _('Your reset password request has expired. You can ask a new one.')
+        
+        msg = self.testPasswordValidity(password, confirm=confirm)
+        if not msg : # None if everything ok. Err message otherwise.
+            mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
+            member = mtool.getMemberById(userid)
+            if member :
+                member.setSecurityProfile(password=password)
+                del self._passwordResetRequests[uuid]
+                return  userid, _('Password successfully reset.')
+            else :
+                return None, _('"%s" username not found.') % userid
+            
+        
 InitializeClass(RegistrationTool)
\ No newline at end of file