f31a5c62ab16bfe695b727736f4d0484f312a51a
[Plinn.git] / RegistrationTool.py
1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Plinn - http://plinn.org #
4 # © 2005-2013 Benoît PIN <pin@cri.ensmp.fr> #
5 # #
6 # This program is free software; you can redistribute it and/or #
7 # modify it under the terms of the GNU General Public License #
8 # as published by the Free Software Foundation; either version 2 #
9 # of the License, or (at your option) any later version. #
10 # #
11 # This program is distributed in the hope that it will be useful, #
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
14 # GNU General Public License for more details. #
15 # #
16 # You should have received a copy of the GNU General Public License #
17 # along with this program; if not, write to the Free Software #
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #
19 #######################################################################################
20 """ Plinn registration tool: implements 3 modes to register members:
21 anonymous, manager, reviewed.
22
23
24
25 """
26
27 from Globals import InitializeClass, PersistentMapping
28 from Products.PageTemplates.PageTemplateFile import PageTemplateFile
29 from Products.CMFDefault.RegistrationTool import RegistrationTool as BaseRegistrationTool
30 from AccessControl import ClassSecurityInfo, ModuleSecurityInfo
31 from AccessControl.Permission import Permission
32 from BTrees.OOBTree import OOBTree
33 from Products.CMFCore.permissions import ManagePortal, AddPortalMember
34 from Products.CMFCore.exceptions import AccessControl_Unauthorized
35 from Products.CMFCore.utils import getToolByName
36 from Products.CMFCore.utils import getUtilityByInterfaceName
37 from Products.GroupUserFolder.GroupsToolPermissions import ManageGroups
38 from Products.Plinn.utils import Message as _
39 from Products.Plinn.utils import encodeQuopriEmail
40 from DateTime import DateTime
41 from types import TupleType, ListType
42 from uuid import uuid4
43 from quopri import encodestring
44
45 security = ModuleSecurityInfo('Products.Plinn.RegistrationTool')
46 MODE_ANONYMOUS = 'anonymous'
47 security.declarePublic('MODE_ANONYMOUS')
48
49 MODE_MANAGER = 'manager'
50 security.declarePublic('MODE_MANAGER')
51
52 MODE_REVIEWED = 'reviewed'
53 security.declarePublic('MODE_REVIEWED')
54
55 MODES = [MODE_ANONYMOUS, MODE_MANAGER, MODE_REVIEWED]
56 security.declarePublic('MODES')
57
58 DEFAULT_MEMBER_GROUP = 'members'
59 security.declarePublic('DEFAULT_MEMBER_GROUP')
60
61
62
63 class RegistrationTool(BaseRegistrationTool) :
64
65 """ Create and modify users by making calls to portal_membership.
66 """
67
68 meta_type = "Plinn Registration Tool"
69
70 manage_options = ({'label' : 'Registration mode', 'action' : 'manage_regmode'}, ) + \
71 BaseRegistrationTool.manage_options
72
73 security = ClassSecurityInfo()
74
75 security.declareProtected( ManagePortal, 'manage_regmode' )
76 manage_regmode = PageTemplateFile('www/configureRegistrationTool', globals(),
77 __name__='manage_regmode')
78
79 def __init__(self) :
80 self._mode = MODE_ANONYMOUS
81 self._chain = ''
82 self._passwordResetRequests = OOBTree()
83
84 security.declareProtected(ManagePortal, 'configureTool')
85 def configureTool(self, registration_mode, chain, REQUEST=None) :
86 """ """
87
88 if registration_mode not in MODES :
89 raise ValueError, "Unknown mode: " + registration_mode
90 else :
91 self._mode = registration_mode
92 self._updatePortalRoleMappingForMode(registration_mode)
93
94 wtool = getToolByName(self, 'portal_workflow')
95
96 if registration_mode == MODE_REVIEWED :
97 if not hasattr(wtool, '_chains_by_type') :
98 wtool._chains_by_type = PersistentMapping()
99 wfids = []
100 chain = chain.strip()
101
102 if chain == '(Default)' :
103 try : del wtool._chains_by_type['Member Data']
104 except KeyError : pass
105 self._chain = chain
106 else :
107 for wfid in chain.replace(',', ' ').split(' ') :
108 if wfid :
109 if not wtool.getWorkflowById(wfid) :
110 raise ValueError, '"%s" is not a workflow ID.' % wfid
111 wfids.append(wfid)
112
113 wtool._chains_by_type['Member Data'] = tuple(wfids)
114 self._chain = ', '.join(wfids)
115 else :
116 wtool._chains_by_type['Member Data'] = tuple()
117
118 if REQUEST :
119 REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_regmode?manage_tabs_message=Saved changes.')
120
121 def _updatePortalRoleMappingForMode(self, mode) :
122
123 urlTool = getToolByName(self, 'portal_url')
124 portal = urlTool.getPortalObject()
125
126 if mode in [MODE_ANONYMOUS, MODE_REVIEWED] :
127 portal.manage_permission(AddPortalMember, roles = ['Anonymous', 'Manager'], acquire=1)
128 elif mode == MODE_MANAGER :
129 portal.manage_permission(AddPortalMember, roles = ['Manager', 'UserManager'], acquire=0)
130
131 security.declarePublic('getMode')
132 def getMode(self) :
133 # """ return current mode """
134 return self._mode[:]
135
136 security.declarePublic('getWfId')
137 def getWfChain(self) :
138 # """ return current workflow id """
139 return self._chain
140
141 security.declarePublic('roleMappingMismatch')
142 def roleMappingMismatch(self) :
143 # """ test if the role mapping is correct for the currrent mode """
144
145 mode = self._mode
146 urlTool = getToolByName(self, 'portal_url')
147 portal = urlTool.getPortalObject()
148
149 def rolesOfAddPortalMemberPerm() :
150 p=Permission(AddPortalMember, [], portal)
151 return p.getRoles()
152
153 if mode in [MODE_ANONYMOUS, MODE_REVIEWED] :
154 if 'Anonymous' in rolesOfAddPortalMemberPerm() : return False
155
156 elif mode == MODE_MANAGER :
157 roles = rolesOfAddPortalMemberPerm()
158 if 'Manager' in roles or 'UserManager' in roles and len(roles) == 1 and type(roles) == TupleType :
159 return False
160
161 return True
162
163 security.declareProtected(AddPortalMember, 'addMember')
164 def addMember(self, id, password, roles=(), groups=(DEFAULT_MEMBER_GROUP,), domains='', properties=None) :
165 """ Idem CMFCore but without default role """
166 BaseRegistrationTool.addMember(self, id, password, roles=roles,
167 domains=domains, properties=properties)
168
169 if self.getMode() in [MODE_ANONYMOUS, MODE_MANAGER] :
170 gtool = getToolByName(self, 'portal_groups')
171 mtool = getToolByName(self, 'portal_membership')
172 utool = getToolByName(self, 'portal_url')
173 portal = utool.getPortalObject()
174 isGrpManager = mtool.checkPermission(ManageGroups, portal) ## TODO : CMF2.1 compat
175 aclu = self.aq_inner.acl_users
176
177 for gid in groups:
178 g = gtool.getGroupById(gid)
179 if not isGrpManager :
180 if gid != DEFAULT_MEMBER_GROUP:
181 raise AccessControl_Unauthorized, 'You are not allowed to join arbitrary group.'
182
183 if g is None :
184 gtool.addGroup(gid)
185 aclu.changeUser(aclu.getGroupPrefix() +gid, roles=['Member', ])
186 g = gtool.getGroupById(gid)
187 g.addMember(id)
188
189
190 def afterAdd(self, member, id, password, properties):
191 """ notify member creation """
192 member.notifyWorkflowCreated()
193 member.indexObject()
194
195
196 security.declarePublic('requestPasswordReset')
197 def requestPasswordReset(self, userid):
198 """ add uuid / (userid, expiration) pair and return uuid """
199 self.clearExpiredPasswordResetRequests()
200 mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
201 member = mtool.getMemberById(userid)
202 if member :
203 uuid = str(uuid4())
204 while self._passwordResetRequests.has_key(uuid) :
205 uuid = str(uuid4())
206 self._passwordResetRequests[uuid] = (userid, DateTime() + 1)
207 utool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IURLTool')
208 ptool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IPropertiesTool')
209 # fuck : mailhost récupéré avec getUtilityByInterfaceName n'est pas correctement
210 # wrappé. Un « unrestrictedTraverse » ne marche pas.
211 # mailhost = getUtilityByInterfaceName('Products.MailHost.interfaces.IMailHost')
212 portal = utool.getPortalObject()
213 mailhost = portal.MailHost
214 sender = encodeQuopriEmail(ptool.getProperty('email_from_name'), ptool.getProperty('email_from_address'))
215 to = encodeQuopriEmail(member.getMemberFullName(nameBefore=0), member.getProperty('email'))
216 subject = "=?utf-8?q?%s?=" % encodestring('Password reset')
217 lines = []
218 pr = lines.append
219 pr('%s/password_reset_form/%s' % (utool(), uuid))
220 body = '\n'.join(lines)
221 message = self.echange_mail_template(From=sender,
222 To=to,
223 Subject=subject,
224 ContentType = 'text/plain',
225 charset = 'UTF-8',
226 body=body)
227 mailhost.send(message)
228 return
229
230 return _('Unknown user name. Please retry.')
231
232 security.declarePrivate('clearExpiredPasswordResetRequests')
233 def clearExpiredPasswordResetRequests(self):
234 now = DateTime()
235 for uuid, record in self._passwordResetRequests.items() :
236 userid, date = record
237 if date < now :
238 del self._passwordResetRequests[uuid]
239
240
241 security.declarePublic('resetPassword')
242 def resetPassword(self, uuid, password, confirm) :
243 record = self._passwordResetRequests.get(uuid)
244 if not record :
245 return _('Invalid reset password request.')
246
247 userid, expiration = record
248 now = DateTime()
249 if expiration < now :
250 self.clearExpiredPasswordResetRequests()
251 return _('Your reset password request has expired. You can ask a new one.')
252
253 msg = self.testPasswordValidity(password, confirm=confirm)
254 if not msg : # None if everything ok. Err message otherwise.
255 mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
256 member = mtool.getMemberById(userid)
257 if member :
258 member.setSecurityProfile(password=password)
259 del self._passwordResetRequests[uuid]
260 return _('Password successfully resetted.')
261 else :
262 return _('"%s" username not found.') % userid
263
264
265 InitializeClass(RegistrationTool)