1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Plinn - http://plinn.org #
4 # © 2005-2013 Benoît PIN <pin@cri.ensmp.fr> #
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. #
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. #
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.
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
49 security
= ModuleSecurityInfo('Products.Plinn.RegistrationTool')
50 MODE_ANONYMOUS
= 'anonymous'
51 security
.declarePublic('MODE_ANONYMOUS')
53 MODE_PASS_ANONYMOUS
= 'pass_anonymous'
54 security
.declarePublic('MODE_PASS_ANONYMOUS')
56 MODE_MANAGER
= 'manager'
57 security
.declarePublic('MODE_MANAGER')
59 MODE_REVIEWED
= 'reviewed'
60 security
.declarePublic('MODE_REVIEWED')
62 MODES
= [MODE_ANONYMOUS
, MODE_PASS_ANONYMOUS
, MODE_MANAGER
, MODE_REVIEWED
]
63 security
.declarePublic('MODES')
65 DEFAULT_MEMBER_GROUP
= 'members'
66 security
.declarePublic('DEFAULT_MEMBER_GROUP')
70 class RegistrationTool(BaseRegistrationTool
) :
72 """ Create and modify users by making calls to portal_membership.
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
)
79 manage_options
= ({'label' : 'Registration mode', 'action' : 'manage_regmode'}, ) + \
80 BaseRegistrationTool
.manage_options
82 security
= ClassSecurityInfo()
84 security
.declareProtected( ManagePortal
, 'manage_regmode' )
85 manage_regmode
= PageTemplateFile('www/configureRegistrationTool', globals(),
86 __name__
='manage_regmode')
89 self
._mode
= MODE_ANONYMOUS
91 self
._passwordResetRequests
= OOBTree()
93 security
.declareProtected(ManagePortal
, 'configureTool')
94 def configureTool(self
, registration_mode
, chain
, REQUEST
=None) :
97 if registration_mode
not in MODES
:
98 raise ValueError, "Unknown mode: " + registration_mode
100 self
._mode
= registration_mode
101 self
._updatePortalRoleMappingForMode
(registration_mode
)
103 wtool
= getToolByName(self
, 'portal_workflow')
105 if registration_mode
== MODE_REVIEWED
:
106 if not hasattr(wtool
, '_chains_by_type') :
107 wtool
._chains
_by
_type
= PersistentMapping()
109 chain
= chain
.strip()
111 if chain
== '(Default)' :
112 try : del wtool
._chains
_by
_type
['Member Data']
113 except KeyError : pass
116 for wfid
in chain
.replace(',', ' ').split(' ') :
118 if not wtool
.getWorkflowById(wfid
) :
119 raise ValueError, '"%s" is not a workflow ID.' % wfid
122 wtool
._chains
_by
_type
['Member Data'] = tuple(wfids
)
123 self
._chain
= ', '.join(wfids
)
125 wtool
._chains
_by
_type
['Member Data'] = tuple()
128 REQUEST
.RESPONSE
.redirect(self
.absolute_url() + '/manage_regmode?manage_tabs_message=Saved changes.')
130 def _updatePortalRoleMappingForMode(self
, mode
) :
132 urlTool
= getToolByName(self
, 'portal_url')
133 portal
= urlTool
.getPortalObject()
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)
140 security
.declarePublic('getMode')
142 # """ return current mode """
145 security
.declarePublic('getWfId')
146 def getWfChain(self
) :
147 # """ return current workflow id """
150 security
.declarePublic('roleMappingMismatch')
151 def roleMappingMismatch(self
) :
152 # """ test if the role mapping is correct for the currrent mode """
155 urlTool
= getToolByName(self
, 'portal_url')
156 portal
= urlTool
.getPortalObject()
158 def rolesOfAddPortalMemberPerm() :
159 p
=Permission(AddPortalMember
, [], portal
)
162 if mode
in [MODE_ANONYMOUS
, MODE_PASS_ANONYMOUS
, MODE_REVIEWED
] :
163 if 'Anonymous' in rolesOfAddPortalMemberPerm() : return False
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
:
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 """
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()
182 if self
.getMode() == MODE_PASS_ANONYMOUS
:
183 private_collections
= portal
.get('private_collections')
184 if not private_collections
:
185 raise AccessControl_Unauthorized()
187 data
= private_collections
.data
188 lines
= filter(None, [l
.strip() for l
in data
.split('\n')])
189 assert len(lines
) % 3 == 0
191 for i
in xrange(0, len(lines
), 3) :
192 collecInfos
[lines
[i
]] = {'pw' : lines
[i
+1],
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')
200 BaseRegistrationTool
.addMember(self
, id, password
, roles
=roles
,
201 domains
=domains
, properties
=properties
)
203 isGrpManager
= mtool
.checkPermission(ManageGroups
, portal
) ## TODO : CMF2.1 compat
204 aclu
= self
.aq_inner
.acl_users
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.'
214 aclu
.changeUser(aclu
.getGroupPrefix() +gid
, roles
=['Member', ])
215 g
= gtool
.getGroupById(gid
)
218 BaseRegistrationTool
.addMember(self
, id, password
, roles
=roles
,
219 domains
=domains
, properties
=properties
)
222 def afterAdd(self
, member
, id, password
, properties
):
223 """ notify member creation """
224 member
.notifyWorkflowCreated()
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.
235 security
.declarePublic('requestPasswordReset')
236 def requestPasswordReset(self
, userid
, initial
=False):
237 """ add uuid / (userid, expiration) pair
238 if ok: send an email to member. returns error message otherwise.
240 self
.clearExpiredPasswordResetRequests()
241 mtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
242 member
= mtool
.getMemberById(userid
)
245 checkEmailAddress(userid
)
246 member
= mtool
.searchMembers('email', userid
)
248 userid
= member
[0]['username']
249 member
= mtool
.getMemberById(userid
)
250 except EmailAddressInvalid
:
254 while self
._passwordResetRequests
.has_key(uuid
) :
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'))
267 subject
= translate(_('Complete your registration on the %s website')) % ptool
.getProperty('title')
269 subject
= translate(_('How to reset your password on the %s website')) % ptool
.getProperty('title')
270 subject
= encodeMailHeader(subject
)
271 options
= {'initial' : initial
,
272 'fullName' : member
.getMemberFullName(nameBefore
=0),
273 'member_id' : member
.getId(),
274 'loginIsNotEmail' : member
.getId() != member
.getProperty('email'),
275 'siteName' : ptool
.getProperty('title'),
276 'resetPasswordUrl' : '%s/password_reset_form/%s' % (utool(), uuid
)}
277 body
= self
.password_reset_mail(options
)
278 message
= self
.echange_mail_template(From
=sender
,
281 ContentType
= 'text/plain',
284 mailhost
.send(message
)
287 return _('Unknown user name. Please retry.')
289 security
.declarePrivate('clearExpiredPasswordResetRequests')
290 def clearExpiredPasswordResetRequests(self
):
292 for uuid
, record
in self
._passwordResetRequests
.items() :
293 userid
, date
= record
295 del self
._passwordResetRequests
[uuid
]
298 security
.declarePublic('resetPassword')
299 def resetPassword(self
, uuid
, password
, confirm
) :
300 record
= self
._passwordResetRequests
.get(uuid
)
302 return None, _('Invalid reset password request.')
304 userid
, expiration
= record
306 if expiration
< now
:
307 self
.clearExpiredPasswordResetRequests()
308 return None, _('Your reset password request has expired. You can ask a new one.')
310 msg
= self
.testPasswordValidity(password
, confirm
=confirm
)
311 if not msg
: # None if everything ok. Err message otherwise.
312 mtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
313 member
= mtool
.getMemberById(userid
)
315 member
.setSecurityProfile(password
=password
)
316 del self
._passwordResetRequests
[uuid
]
317 return userid
, _('Password successfully updated.')
319 return None, _('"%s" username not found.') % userid
323 InitializeClass(RegistrationTool
)