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