fff500e8ba1ffde85fb703d63cce0cb4867f805d
[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
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
198
199 BaseRegistrationTool.addMember(self, id, password, roles=roles,
200 domains=domains, properties=properties)
201
202 isGrpManager = mtool.checkPermission(ManageGroups, portal) ## TODO : CMF2.1 compat
203 aclu = self.aq_inner.acl_users
204
205 for gid in groups:
206 g = gtool.getGroupById(gid)
207 if not isGrpManager :
208 if gid != DEFAULT_MEMBER_GROUP:
209 raise AccessControl_Unauthorized, 'You are not allowed to join arbitrary group.'
210
211 if g is None :
212 gtool.addGroup(gid)
213 aclu.changeUser(aclu.getGroupPrefix() +gid, roles=['Member', ])
214 g = gtool.getGroupById(gid)
215 g.addMember(id)
216 else :
217 BaseRegistrationTool.addMember(self, id, password, roles=roles,
218 domains=domains, properties=properties)
219
220
221 def afterAdd(self, member, id, password, properties):
222 """ notify member creation """
223 member.notifyWorkflowCreated()
224 member.indexObject()
225
226 security.declarePublic('generatePassword')
227 def generatePassword(self):
228 """ This password may not been entered by user.
229 Password generated by this method are typicaly used
230 on a member registration with 'validate_email' option enabled.
231 """
232 return str(uuid4())
233
234 security.declarePublic('requestPasswordReset')
235 def requestPasswordReset(self, userid, initial=False):
236 """ add uuid / (userid, expiration) pair
237 if ok: send an email to member. returns error message otherwise.
238 """
239 self.clearExpiredPasswordResetRequests()
240 mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
241 member = mtool.getMemberById(userid)
242 if not member :
243 try :
244 checkEmailAddress(userid)
245 member = mtool.searchMembers('email', userid)
246 if member :
247 userid = member[0]['username']
248 member = mtool.getMemberById(userid)
249 except EmailAddressInvalid :
250 pass
251 if member :
252 uuid = str(uuid4())
253 while self._passwordResetRequests.has_key(uuid) :
254 uuid = str(uuid4())
255 self._passwordResetRequests[uuid] = (userid, DateTime() + 1)
256 utool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IURLTool')
257 ptool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IPropertiesTool')
258 # fuck : mailhost récupéré avec getUtilityByInterfaceName n'est pas correctement
259 # wrappé. Un « unrestrictedTraverse » ne marche pas.
260 # mailhost = getUtilityByInterfaceName('Products.MailHost.interfaces.IMailHost')
261 portal = utool.getPortalObject()
262 mailhost = portal.MailHost
263 sender = encodeQuopriEmail(ptool.getProperty('email_from_name'), ptool.getProperty('email_from_address'))
264 to = encodeQuopriEmail(member.getMemberFullName(nameBefore=0), member.getProperty('email'))
265 if initial :
266 subject = translate(_('Complete your registration on the %s website')) % ptool.getProperty('title')
267 else :
268 subject = translate(_('How to reset your password on the %s website')) % ptool.getProperty('title')
269 subject = encodeMailHeader(subject)
270 options = {'initial' : initial,
271 'fullName' : member.getMemberFullName(nameBefore=0),
272 'member_id' : member.getId(),
273 'loginIsNotEmail' : member.getId() != member.getProperty('email'),
274 'siteName' : ptool.getProperty('title'),
275 'resetPasswordUrl' : '%s/password_reset_form/%s' % (utool(), uuid)}
276 body = self.password_reset_mail(options)
277 message = self.echange_mail_template(From=sender,
278 To=to,
279 Subject=subject,
280 ContentType = 'text/plain',
281 charset = 'UTF-8',
282 body=body)
283 mailhost.send(message)
284 return
285
286 return _('Unknown user name. Please retry.')
287
288 security.declarePrivate('clearExpiredPasswordResetRequests')
289 def clearExpiredPasswordResetRequests(self):
290 now = DateTime()
291 for uuid, record in self._passwordResetRequests.items() :
292 userid, date = record
293 if date < now :
294 del self._passwordResetRequests[uuid]
295
296
297 security.declarePublic('resetPassword')
298 def resetPassword(self, uuid, password, confirm) :
299 record = self._passwordResetRequests.get(uuid)
300 if not record :
301 return None, _('Invalid reset password request.')
302
303 userid, expiration = record
304 now = DateTime()
305 if expiration < now :
306 self.clearExpiredPasswordResetRequests()
307 return None, _('Your reset password request has expired. You can ask a new one.')
308
309 msg = self.testPasswordValidity(password, confirm=confirm)
310 if not msg : # None if everything ok. Err message otherwise.
311 mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
312 member = mtool.getMemberById(userid)
313 if member :
314 member.setSecurityProfile(password=password)
315 del self._passwordResetRequests[uuid]
316 return userid, _('Password successfully updated.')
317 else :
318 return None, _('"${userid}" username not found.', mapping={'userid': userid})
319 else :
320 return None, msg
321
322 InitializeClass(RegistrationTool)