Refactoring API de IndexIterator.
[Plinn.git] / RegistrationTool.py
index 77f8bdc..8c2911d 100644 (file)
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 #######################################################################################
 #   Plinn - http://plinn.org                                                          #
 # -*- 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                       #
 #                                                                                     #
 #   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.   #
 #######################################################################################
 #   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,23 +29,36 @@ 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 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.permissions import ManagePortal, AddPortalMember
 from Products.CMFCore.exceptions import AccessControl_Unauthorized
+from Products.CMFDefault.exceptions import EmailAddressInvalid
 from Products.CMFCore.utils import getToolByName
 from Products.CMFCore.utils import getToolByName
+from Products.CMFCore.utils import getUtilityByInterfaceName
+from Products.CMFDefault.utils import checkEmailAddress
 from Products.GroupUserFolder.GroupsToolPermissions import ManageGroups
 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 types import TupleType, ListType
+from uuid import uuid4
 
 security = ModuleSecurityInfo('Products.Plinn.RegistrationTool')
 MODE_ANONYMOUS = 'anonymous'
 security.declarePublic('MODE_ANONYMOUS')
 
 
 security = ModuleSecurityInfo('Products.Plinn.RegistrationTool')
 MODE_ANONYMOUS = 'anonymous'
 security.declarePublic('MODE_ANONYMOUS')
 
+MODE_PASS_ANONYMOUS = 'pass_anonymous'
+security.declarePublic('MODE_PASS_ANONYMOUS')
+
 MODE_MANAGER = 'manager'
 security.declarePublic('MODE_MANAGER')
 
 MODE_REVIEWED = 'reviewed'
 security.declarePublic('MODE_REVIEWED')
 
 MODE_MANAGER = 'manager'
 security.declarePublic('MODE_MANAGER')
 
 MODE_REVIEWED = 'reviewed'
 security.declarePublic('MODE_REVIEWED')
 
-MODES = [MODE_ANONYMOUS, MODE_MANAGER, MODE_REVIEWED]
+MODES = [MODE_ANONYMOUS, MODE_PASS_ANONYMOUS, MODE_MANAGER, MODE_REVIEWED]
 security.declarePublic('MODES')
 
 DEFAULT_MEMBER_GROUP = 'members'
 security.declarePublic('MODES')
 
 DEFAULT_MEMBER_GROUP = 'members'
@@ -55,133 +68,237 @@ security.declarePublic('DEFAULT_MEMBER_GROUP')
 
 class RegistrationTool(BaseRegistrationTool) :
 
 
 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_PASS_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_PASS_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 """
+
+        if self.getMode() != MODE_REVIEWED :
+            gtool = getToolByName(self, 'portal_groups')
+            mtool = getToolByName(self, 'portal_membership')
+            utool = getToolByName(self, 'portal_url')
+            portal = utool.getPortalObject()
+            
+            if self.getMode() == MODE_PASS_ANONYMOUS :
+                private_collections = portal.get('private_collections')
+                if not private_collections :
+                    raise AccessControl_Unauthorized()
+                    return
+                data = private_collections.data
+                lines = filter(None, [l.strip() for l in data.split('\n')])
+                assert len(lines) % 3 == 0
+                collecInfos = {}
+                for i in xrange(0, len(lines), 3) :
+                    collecInfos[lines[i]] = {'pw' : lines[i+1],
+                                             'path' : lines[i+2]}
+                if not (collecInfos.has_key(properties.get('collection_id')) and \
+                    collecInfos[properties.get('collection_id')]['pw'] == properties.get('collection_password')) :
+                    raise AccessControl_Unauthorized('Wrong primary credentials')
+                    return
+            
+            
+            BaseRegistrationTool.addMember(self, id, password, roles=roles,
+                                           domains=domains, properties=properties)
+
+            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)
+        else :
+            BaseRegistrationTool.addMember(self, id, password, roles=roles,
+                                           domains=domains, properties=properties)
+
+
+    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 not member :
+            try :
+                checkEmailAddress(userid)
+                member = mtool.searchMembers('email', userid)
+                if member :
+                    userid = member[0]['username']
+                    member = mtool.getMemberById(userid)
+            except EmailAddressInvalid :
+                pass
+        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 updated.')
+            else :
+                return None, _('"%s" username not found.') % userid
+            
+        
 InitializeClass(RegistrationTool)
\ No newline at end of file
 InitializeClass(RegistrationTool)
\ No newline at end of file