Refactoring, commentaires.
[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 translate
40 from Products.Plinn.utils import encodeQuopriEmail
41 from Products.Plinn.utils import encodeMailHeader
42 from DateTime import DateTime
43 from types import TupleType, ListType
44 from uuid import uuid4
45
46 security = ModuleSecurityInfo('Products.Plinn.RegistrationTool')
47 MODE_ANONYMOUS = 'anonymous'
48 security.declarePublic('MODE_ANONYMOUS')
49
50 MODE_MANAGER = 'manager'
51 security.declarePublic('MODE_MANAGER')
52
53 MODE_REVIEWED = 'reviewed'
54 security.declarePublic('MODE_REVIEWED')
55
56 MODES = [MODE_ANONYMOUS, MODE_MANAGER, MODE_REVIEWED]
57 security.declarePublic('MODES')
58
59 DEFAULT_MEMBER_GROUP = 'members'
60 security.declarePublic('DEFAULT_MEMBER_GROUP')
61
62
63
64 class RegistrationTool(BaseRegistrationTool) :
65
66 """ Create and modify users by making calls to portal_membership.
67 """
68
69 meta_type = "Plinn Registration Tool"
70
71 manage_options = ({'label' : 'Registration mode', 'action' : 'manage_regmode'}, ) + \
72 BaseRegistrationTool.manage_options
73
74 security = ClassSecurityInfo()
75
76 security.declareProtected( ManagePortal, 'manage_regmode' )
77 manage_regmode = PageTemplateFile('www/configureRegistrationTool', globals(),
78 __name__='manage_regmode')
79
80 def __init__(self) :
81 self._mode = MODE_ANONYMOUS
82 self._chain = ''
83 self._passwordResetRequests = OOBTree()
84
85 security.declareProtected(ManagePortal, 'configureTool')
86 def configureTool(self, registration_mode, chain, REQUEST=None) :
87 """ """
88
89 if registration_mode not in MODES :
90 raise ValueError, "Unknown mode: " + registration_mode
91 else :
92 self._mode = registration_mode
93 self._updatePortalRoleMappingForMode(registration_mode)
94
95 wtool = getToolByName(self, 'portal_workflow')
96
97 if registration_mode == MODE_REVIEWED :
98 if not hasattr(wtool, '_chains_by_type') :
99 wtool._chains_by_type = PersistentMapping()
100 wfids = []
101 chain = chain.strip()
102
103 if chain == '(Default)' :
104 try : del wtool._chains_by_type['Member Data']
105 except KeyError : pass
106 self._chain = chain
107 else :
108 for wfid in chain.replace(',', ' ').split(' ') :
109 if wfid :
110 if not wtool.getWorkflowById(wfid) :
111 raise ValueError, '"%s" is not a workflow ID.' % wfid
112 wfids.append(wfid)
113
114 wtool._chains_by_type['Member Data'] = tuple(wfids)
115 self._chain = ', '.join(wfids)
116 else :
117 wtool._chains_by_type['Member Data'] = tuple()
118
119 if REQUEST :
120 REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_regmode?manage_tabs_message=Saved changes.')
121
122 def _updatePortalRoleMappingForMode(self, mode) :
123
124 urlTool = getToolByName(self, 'portal_url')
125 portal = urlTool.getPortalObject()
126
127 if mode in [MODE_ANONYMOUS, MODE_REVIEWED] :
128 portal.manage_permission(AddPortalMember, roles = ['Anonymous', 'Manager'], acquire=1)
129 elif mode == MODE_MANAGER :
130 portal.manage_permission(AddPortalMember, roles = ['Manager', 'UserManager'], acquire=0)
131
132 security.declarePublic('getMode')
133 def getMode(self) :
134 # """ return current mode """
135 return self._mode[:]
136
137 security.declarePublic('getWfId')
138 def getWfChain(self) :
139 # """ return current workflow id """
140 return self._chain
141
142 security.declarePublic('roleMappingMismatch')
143 def roleMappingMismatch(self) :
144 # """ test if the role mapping is correct for the currrent mode """
145
146 mode = self._mode
147 urlTool = getToolByName(self, 'portal_url')
148 portal = urlTool.getPortalObject()
149
150 def rolesOfAddPortalMemberPerm() :
151 p=Permission(AddPortalMember, [], portal)
152 return p.getRoles()
153
154 if mode in [MODE_ANONYMOUS, MODE_REVIEWED] :
155 if 'Anonymous' in rolesOfAddPortalMemberPerm() : return False
156
157 elif mode == MODE_MANAGER :
158 roles = rolesOfAddPortalMemberPerm()
159 if 'Manager' in roles or 'UserManager' in roles and len(roles) == 1 and type(roles) == TupleType :
160 return False
161
162 return True
163
164 security.declareProtected(AddPortalMember, 'addMember')
165 def addMember(self, id, password, roles=(), groups=(DEFAULT_MEMBER_GROUP,), domains='', properties=None) :
166 """ Idem CMFCore but without default role """
167 BaseRegistrationTool.addMember(self, id, password, roles=roles,
168 domains=domains, properties=properties)
169
170 if self.getMode() in [MODE_ANONYMOUS, MODE_MANAGER] :
171 gtool = getToolByName(self, 'portal_groups')
172 mtool = getToolByName(self, 'portal_membership')
173 utool = getToolByName(self, 'portal_url')
174 portal = utool.getPortalObject()
175 isGrpManager = mtool.checkPermission(ManageGroups, portal) ## TODO : CMF2.1 compat
176 aclu = self.aq_inner.acl_users
177
178 for gid in groups:
179 g = gtool.getGroupById(gid)
180 if not isGrpManager :
181 if gid != DEFAULT_MEMBER_GROUP:
182 raise AccessControl_Unauthorized, 'You are not allowed to join arbitrary group.'
183
184 if g is None :
185 gtool.addGroup(gid)
186 aclu.changeUser(aclu.getGroupPrefix() +gid, roles=['Member', ])
187 g = gtool.getGroupById(gid)
188 g.addMember(id)
189
190
191 def afterAdd(self, member, id, password, properties):
192 """ notify member creation """
193 member.notifyWorkflowCreated()
194 member.indexObject()
195
196
197 security.declarePublic('requestPasswordReset')
198 def requestPasswordReset(self, userid):
199 """ add uuid / (userid, expiration) pair and return uuid """
200 self.clearExpiredPasswordResetRequests()
201 mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
202 member = mtool.getMemberById(userid)
203 if member :
204 uuid = str(uuid4())
205 while self._passwordResetRequests.has_key(uuid) :
206 uuid = str(uuid4())
207 self._passwordResetRequests[uuid] = (userid, DateTime() + 1)
208 utool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IURLTool')
209 ptool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IPropertiesTool')
210 # fuck : mailhost récupéré avec getUtilityByInterfaceName n'est pas correctement
211 # wrappé. Un « unrestrictedTraverse » ne marche pas.
212 # mailhost = getUtilityByInterfaceName('Products.MailHost.interfaces.IMailHost')
213 portal = utool.getPortalObject()
214 mailhost = portal.MailHost
215 sender = encodeQuopriEmail(ptool.getProperty('email_from_name'), ptool.getProperty('email_from_address'))
216 to = encodeQuopriEmail(member.getMemberFullName(nameBefore=0), member.getProperty('email'))
217 subject = translate(_('How to reset your password on the %s website')) % ptool.getProperty('title')
218 subject = encodeMailHeader(subject)
219 options = {'fullName' : member.getMemberFullName(nameBefore=0),
220 'siteName' : ptool.getProperty('title'),
221 'resetPasswordUrl' : '%s/password_reset_form/%s' % (utool(), uuid)}
222 body = self.password_reset_mail(options)
223 message = self.echange_mail_template(From=sender,
224 To=to,
225 Subject=subject,
226 ContentType = 'text/plain',
227 charset = 'UTF-8',
228 body=body)
229 mailhost.send(message)
230 return
231
232 return _('Unknown user name. Please retry.')
233
234 security.declarePrivate('clearExpiredPasswordResetRequests')
235 def clearExpiredPasswordResetRequests(self):
236 now = DateTime()
237 for uuid, record in self._passwordResetRequests.items() :
238 userid, date = record
239 if date < now :
240 del self._passwordResetRequests[uuid]
241
242
243 security.declarePublic('resetPassword')
244 def resetPassword(self, uuid, password, confirm) :
245 record = self._passwordResetRequests.get(uuid)
246 if not record :
247 return None, _('Invalid reset password request.')
248
249 userid, expiration = record
250 now = DateTime()
251 if expiration < now :
252 self.clearExpiredPasswordResetRequests()
253 return None, _('Your reset password request has expired. You can ask a new one.')
254
255 msg = self.testPasswordValidity(password, confirm=confirm)
256 if not msg : # None if everything ok. Err message otherwise.
257 mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
258 member = mtool.getMemberById(userid)
259 if member :
260 member.setSecurityProfile(password=password)
261 del self._passwordResetRequests[uuid]
262 return userid, _('Password successfully reset.')
263 else :
264 return None, _('"%s" username not found.') % userid
265
266
267 InitializeClass(RegistrationTool)