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