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