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
.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
50 security
= ModuleSecurityInfo('Products.Plinn.RegistrationTool')
51 MODE_ANONYMOUS
= 'anonymous'
52 security
.declarePublic('MODE_ANONYMOUS')
54 MODE_PASS_ANONYMOUS
= 'pass_anonymous'
55 security
.declarePublic('MODE_PASS_ANONYMOUS')
57 MODE_MANAGER
= 'manager'
58 security
.declarePublic('MODE_MANAGER')
60 MODE_REVIEWED
= 'reviewed'
61 security
.declarePublic('MODE_REVIEWED')
63 MODES
= [MODE_ANONYMOUS
, MODE_PASS_ANONYMOUS
, MODE_MANAGER
, MODE_REVIEWED
]
64 security
.declarePublic('MODES')
66 DEFAULT_MEMBER_GROUP
= 'members'
67 security
.declarePublic('DEFAULT_MEMBER_GROUP')
71 class RegistrationTool(BaseRegistrationTool
) :
73 """ Create and modify users by making calls to portal_membership.
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
)
80 manage_options
= ({'label' : 'Registration mode', 'action' : 'manage_regmode'}, ) + \
81 BaseRegistrationTool
.manage_options
83 security
= ClassSecurityInfo()
85 security
.declareProtected( ManagePortal
, 'manage_regmode' )
86 manage_regmode
= PageTemplateFile('www/configureRegistrationTool', globals(),
87 __name__
='manage_regmode')
90 self
._mode
= MODE_ANONYMOUS
92 self
._passwordResetRequests
= OOBTree()
94 security
.declareProtected(ManagePortal
, 'configureTool')
95 def configureTool(self
, registration_mode
, chain
, REQUEST
=None) :
98 if registration_mode
not in MODES
:
99 raise ValueError, "Unknown mode: " + registration_mode
101 self
._mode
= registration_mode
102 self
._updatePortalRoleMappingForMode
(registration_mode
)
104 wtool
= getToolByName(self
, 'portal_workflow')
106 if registration_mode
== MODE_REVIEWED
:
107 if not hasattr(wtool
, '_chains_by_type') :
108 wtool
._chains
_by
_type
= PersistentMapping()
110 chain
= chain
.strip()
112 if chain
== '(Default)' :
113 try : del wtool
._chains
_by
_type
['Member Data']
114 except KeyError : pass
117 for wfid
in chain
.replace(',', ' ').split(' ') :
119 if not wtool
.getWorkflowById(wfid
) :
120 raise ValueError, '"%s" is not a workflow ID.' % wfid
123 wtool
._chains
_by
_type
['Member Data'] = tuple(wfids
)
124 self
._chain
= ', '.join(wfids
)
126 wtool
._chains
_by
_type
['Member Data'] = tuple()
129 REQUEST
.RESPONSE
.redirect(self
.absolute_url() + '/manage_regmode?manage_tabs_message=Saved changes.')
131 def _updatePortalRoleMappingForMode(self
, mode
) :
133 urlTool
= getToolByName(self
, 'portal_url')
134 portal
= urlTool
.getPortalObject()
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)
141 security
.declarePublic('getMode')
143 # """ return current mode """
146 security
.declarePublic('getWfId')
147 def getWfChain(self
) :
148 # """ return current workflow id """
151 security
.declarePublic('roleMappingMismatch')
152 def roleMappingMismatch(self
) :
153 # """ test if the role mapping is correct for the currrent mode """
156 urlTool
= getToolByName(self
, 'portal_url')
157 portal
= urlTool
.getPortalObject()
159 def rolesOfAddPortalMemberPerm() :
160 p
=Permission(AddPortalMember
, [], portal
)
163 if mode
in [MODE_ANONYMOUS
, MODE_PASS_ANONYMOUS
, MODE_REVIEWED
] :
164 if 'Anonymous' in rolesOfAddPortalMemberPerm() : return False
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
:
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 """
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()
183 if self
.getMode() == MODE_PASS_ANONYMOUS
:
184 private_collections
= portal
.get('private_collections')
185 if not private_collections
:
186 raise AccessControl_Unauthorized()
188 data
= private_collections
.data
189 lines
= filter(None, [l
.strip() for l
in data
.split('\n')])
190 assert len(lines
) % 3 == 0
192 for i
in xrange(0, len(lines
), 3) :
193 collecInfos
[lines
[i
]] = {'pw' : lines
[i
+1],
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')
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
)
221 security
.declarePublic( 'testPasswordValidity' )
222 def testPasswordValidity(self
, password
, confirm
=None):
224 """ Verify that the password satisfies the portal's requirements.
226 o If the password is valid, return None.
227 o If not, return a string explaining why.
230 return _(u
'You must enter a password.')
232 if len(password
) < 8 and not _checkPermission(ManagePortal
, self
):
233 return _(u
'Your password must contain at least 8 characters.')
235 if confirm
is not None and confirm
!= password
:
236 return _(u
'Your password and confirmation did not match. '
237 u
'Please try again.')
243 def afterAdd(self
, member
, id, password
, properties
):
244 """ notify member creation """
245 member
.notifyWorkflowCreated()
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.
256 security
.declarePublic('requestPasswordReset')
257 def requestPasswordReset(self
, userid
, initial
=False, came_from
=''):
258 """ add uuid / (userid, expiration) pair
259 if ok: send an email to member. returns error message otherwise.
261 self
.clearExpiredPasswordResetRequests()
262 mtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
263 member
= mtool
.getMemberById(userid
)
266 checkEmailAddress(userid
)
267 member
= mtool
.searchMembers('email', userid
)
269 userid
= member
[0]['username']
270 member
= mtool
.getMemberById(userid
)
271 except EmailAddressInvalid
:
275 while self
._passwordResetRequests
.has_key(uuid
) :
277 self
._passwordResetRequests
[uuid
] = (userid
, DateTime() + 1, came_from
)
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'))
288 subject
= translate(_('Complete your registration on the %s website')) % ptool
.getProperty('title')
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
,
302 ContentType
= 'text/plain',
305 mailhost
.send(message
)
308 return _('Unknown user name. Please retry.')
310 security
.declarePrivate('clearExpiredPasswordResetRequests')
311 def clearExpiredPasswordResetRequests(self
):
313 for uuid
, record
in self
._passwordResetRequests
.items() :
316 del self
._passwordResetRequests
[uuid
]
319 security
.declarePublic('resetPassword')
320 def resetPassword(self
, uuid
, password
, confirm
) :
321 record
= self
._passwordResetRequests
.get(uuid
)
323 return None, _('Invalid reset password request.')
325 userid
, expiration
, came_from
= record
327 if expiration
< now
:
328 self
.clearExpiredPasswordResetRequests()
329 return None, _('Your reset password request has expired. You can ask a new one.')
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
)
336 member
.setSecurityProfile(password
=password
)
337 del self
._passwordResetRequests
[uuid
]
338 return {'userid': userid
, 'came_from' : came_from
}, _('Password successfully updated.')
340 return None, _('"${userid}" username not found.', mapping
={'userid': userid
})
344 InitializeClass(RegistrationTool
)