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