Écrire du HTML n'est pas une maladie honteuse.
[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
173 if self.getMode() != MODE_REVIEWED :
174 gtool = getToolByName(self, 'portal_groups')
175 mtool = getToolByName(self, 'portal_membership')
176 utool = getToolByName(self, 'portal_url')
177 portal = utool.getPortalObject()
178
179 if self.getMode() == MODE_PASS_ANONYMOUS :
180 private_collections = portal.get('private_collections')
181 if not private_collections :
182 raise AccessControl_Unauthorized()
183 return
184 data = private_collections.data
185 lines = filter(None, [l.strip() for l in data.split('\n')])
186 assert len(lines) % 3 == 0
187 collecInfos = {}
188 for i in xrange(0, len(lines), 3) :
189 collecInfos[lines[i]] = {'pw' : lines[i+1],
190 'path' : lines[i+2]}
191 if not (collecInfos.has_key(properties.get('collection_id')) and \
192 collecInfos[properties.get('collection_id')]['pw'] == properties.get('collection_password')) :
193 raise AccessControl_Unauthorized('Wrong primary credentials')
194 return
195
196
197 BaseRegistrationTool.addMember(self, id, password, roles=roles,
198 domains=domains, properties=properties)
199
200 isGrpManager = mtool.checkPermission(ManageGroups, portal) ## TODO : CMF2.1 compat
201 aclu = self.aq_inner.acl_users
202
203 for gid in groups:
204 g = gtool.getGroupById(gid)
205 if not isGrpManager :
206 if gid != DEFAULT_MEMBER_GROUP:
207 raise AccessControl_Unauthorized, 'You are not allowed to join arbitrary group.'
208
209 if g is None :
210 gtool.addGroup(gid)
211 aclu.changeUser(aclu.getGroupPrefix() +gid, roles=['Member', ])
212 g = gtool.getGroupById(gid)
213 g.addMember(id)
214 else :
215 BaseRegistrationTool.addMember(self, id, password, roles=roles,
216 domains=domains, properties=properties)
217
218
219 def afterAdd(self, member, id, password, properties):
220 """ notify member creation """
221 member.notifyWorkflowCreated()
222 member.indexObject()
223
224
225 security.declarePublic('requestPasswordReset')
226 def requestPasswordReset(self, userid):
227 """ add uuid / (userid, expiration) pair and return uuid """
228 self.clearExpiredPasswordResetRequests()
229 mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
230 member = mtool.getMemberById(userid)
231 if not member :
232 try :
233 checkEmailAddress(userid)
234 member = mtool.searchMembers('email', userid)
235 if member :
236 userid = member[0]['username']
237 member = mtool.getMemberById(userid)
238 except EmailAddressInvalid :
239 pass
240 if member :
241 uuid = str(uuid4())
242 while self._passwordResetRequests.has_key(uuid) :
243 uuid = str(uuid4())
244 self._passwordResetRequests[uuid] = (userid, DateTime() + 1)
245 utool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IURLTool')
246 ptool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IPropertiesTool')
247 # fuck : mailhost récupéré avec getUtilityByInterfaceName n'est pas correctement
248 # wrappé. Un « unrestrictedTraverse » ne marche pas.
249 # mailhost = getUtilityByInterfaceName('Products.MailHost.interfaces.IMailHost')
250 portal = utool.getPortalObject()
251 mailhost = portal.MailHost
252 sender = encodeQuopriEmail(ptool.getProperty('email_from_name'), ptool.getProperty('email_from_address'))
253 to = encodeQuopriEmail(member.getMemberFullName(nameBefore=0), member.getProperty('email'))
254 subject = translate(_('How to reset your password on the %s website')) % ptool.getProperty('title')
255 subject = encodeMailHeader(subject)
256 options = {'fullName' : member.getMemberFullName(nameBefore=0),
257 'siteName' : ptool.getProperty('title'),
258 'resetPasswordUrl' : '%s/password_reset_form/%s' % (utool(), uuid)}
259 body = self.password_reset_mail(options)
260 message = self.echange_mail_template(From=sender,
261 To=to,
262 Subject=subject,
263 ContentType = 'text/plain',
264 charset = 'UTF-8',
265 body=body)
266 mailhost.send(message)
267 return
268
269 return _('Unknown user name. Please retry.')
270
271 security.declarePrivate('clearExpiredPasswordResetRequests')
272 def clearExpiredPasswordResetRequests(self):
273 now = DateTime()
274 for uuid, record in self._passwordResetRequests.items() :
275 userid, date = record
276 if date < now :
277 del self._passwordResetRequests[uuid]
278
279
280 security.declarePublic('resetPassword')
281 def resetPassword(self, uuid, password, confirm) :
282 record = self._passwordResetRequests.get(uuid)
283 if not record :
284 return None, _('Invalid reset password request.')
285
286 userid, expiration = record
287 now = DateTime()
288 if expiration < now :
289 self.clearExpiredPasswordResetRequests()
290 return None, _('Your reset password request has expired. You can ask a new one.')
291
292 msg = self.testPasswordValidity(password, confirm=confirm)
293 if not msg : # None if everything ok. Err message otherwise.
294 mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
295 member = mtool.getMemberById(userid)
296 if member :
297 member.setSecurityProfile(password=password)
298 del self._passwordResetRequests[uuid]
299 return userid, _('Password successfully updated.')
300 else :
301 return None, _('"%s" username not found.') % userid
302
303
304 InitializeClass(RegistrationTool)