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
48 security
= ModuleSecurityInfo('Products.Plinn.RegistrationTool')
49 MODE_ANONYMOUS
= 'anonymous'
50 security
.declarePublic('MODE_ANONYMOUS')
52 MODE_PASS_ANONYMOUS
= 'pass_anonymous'
53 security
.declarePublic('MODE_PASS_ANONYMOUS')
55 MODE_MANAGER
= 'manager'
56 security
.declarePublic('MODE_MANAGER')
58 MODE_REVIEWED
= 'reviewed'
59 security
.declarePublic('MODE_REVIEWED')
61 MODES
= [MODE_ANONYMOUS
, MODE_PASS_ANONYMOUS
, MODE_MANAGER
, MODE_REVIEWED
]
62 security
.declarePublic('MODES')
64 DEFAULT_MEMBER_GROUP
= 'members'
65 security
.declarePublic('DEFAULT_MEMBER_GROUP')
69 class RegistrationTool(BaseRegistrationTool
) :
71 """ Create and modify users by making calls to portal_membership.
74 meta_type
= "Plinn Registration Tool"
76 manage_options
= ({'label' : 'Registration mode', 'action' : 'manage_regmode'}, ) + \
77 BaseRegistrationTool
.manage_options
79 security
= ClassSecurityInfo()
81 security
.declareProtected( ManagePortal
, 'manage_regmode' )
82 manage_regmode
= PageTemplateFile('www/configureRegistrationTool', globals(),
83 __name__
='manage_regmode')
86 self
._mode
= MODE_ANONYMOUS
88 self
._passwordResetRequests
= OOBTree()
90 security
.declareProtected(ManagePortal
, 'configureTool')
91 def configureTool(self
, registration_mode
, chain
, REQUEST
=None) :
94 if registration_mode
not in MODES
:
95 raise ValueError, "Unknown mode: " + registration_mode
97 self
._mode
= registration_mode
98 self
._updatePortalRoleMappingForMode
(registration_mode
)
100 wtool
= getToolByName(self
, 'portal_workflow')
102 if registration_mode
== MODE_REVIEWED
:
103 if not hasattr(wtool
, '_chains_by_type') :
104 wtool
._chains
_by
_type
= PersistentMapping()
106 chain
= chain
.strip()
108 if chain
== '(Default)' :
109 try : del wtool
._chains
_by
_type
['Member Data']
110 except KeyError : pass
113 for wfid
in chain
.replace(',', ' ').split(' ') :
115 if not wtool
.getWorkflowById(wfid
) :
116 raise ValueError, '"%s" is not a workflow ID.' % wfid
119 wtool
._chains
_by
_type
['Member Data'] = tuple(wfids
)
120 self
._chain
= ', '.join(wfids
)
122 wtool
._chains
_by
_type
['Member Data'] = tuple()
125 REQUEST
.RESPONSE
.redirect(self
.absolute_url() + '/manage_regmode?manage_tabs_message=Saved changes.')
127 def _updatePortalRoleMappingForMode(self
, mode
) :
129 urlTool
= getToolByName(self
, 'portal_url')
130 portal
= urlTool
.getPortalObject()
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)
137 security
.declarePublic('getMode')
139 # """ return current mode """
142 security
.declarePublic('getWfId')
143 def getWfChain(self
) :
144 # """ return current workflow id """
147 security
.declarePublic('roleMappingMismatch')
148 def roleMappingMismatch(self
) :
149 # """ test if the role mapping is correct for the currrent mode """
152 urlTool
= getToolByName(self
, 'portal_url')
153 portal
= urlTool
.getPortalObject()
155 def rolesOfAddPortalMemberPerm() :
156 p
=Permission(AddPortalMember
, [], portal
)
159 if mode
in [MODE_ANONYMOUS
, MODE_PASS_ANONYMOUS
, MODE_REVIEWED
] :
160 if 'Anonymous' in rolesOfAddPortalMemberPerm() : return False
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
:
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 """
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()
179 if self
.getMode() == MODE_PASS_ANONYMOUS
:
180 private_collections
= portal
.get('private_collections')
181 if not private_collections
:
182 raise AccessControl_Unauthorized()
184 data
= private_collections
.data
185 lines
= filter(None, [l
.strip() for l
in data
.split('\n')])
186 assert len(lines
) % 3 == 0
188 for i
in xrange(0, len(lines
), 3) :
189 collecInfos
[lines
[i
]] = {'pw' : lines
[i
+1],
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')
197 BaseRegistrationTool
.addMember(self
, id, password
, roles
=roles
,
198 domains
=domains
, properties
=properties
)
200 isGrpManager
= mtool
.checkPermission(ManageGroups
, portal
) ## TODO : CMF2.1 compat
201 aclu
= self
.aq_inner
.acl_users
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.'
211 aclu
.changeUser(aclu
.getGroupPrefix() +gid
, roles
=['Member', ])
212 g
= gtool
.getGroupById(gid
)
215 BaseRegistrationTool
.addMember(self
, id, password
, roles
=roles
,
216 domains
=domains
, properties
=properties
)
219 def afterAdd(self
, member
, id, password
, properties
):
220 """ notify member creation """
221 member
.notifyWorkflowCreated()
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
)
233 checkEmailAddress(userid
)
234 member
= mtool
.searchMembers('email', userid
)
236 userid
= member
[0]['username']
237 member
= mtool
.getMemberById(userid
)
238 except EmailAddressInvalid
:
242 while self
._passwordResetRequests
.has_key(uuid
) :
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
,
263 ContentType
= 'text/plain',
266 mailhost
.send(message
)
269 return _('Unknown user name. Please retry.')
271 security
.declarePrivate('clearExpiredPasswordResetRequests')
272 def clearExpiredPasswordResetRequests(self
):
274 for uuid
, record
in self
._passwordResetRequests
.items() :
275 userid
, date
= record
277 del self
._passwordResetRequests
[uuid
]
280 security
.declarePublic('resetPassword')
281 def resetPassword(self
, uuid
, password
, confirm
) :
282 record
= self
._passwordResetRequests
.get(uuid
)
284 return None, _('Invalid reset password request.')
286 userid
, expiration
= record
288 if expiration
< now
:
289 self
.clearExpiredPasswordResetRequests()
290 return None, _('Your reset password request has expired. You can ask a new one.')
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
)
297 member
.setSecurityProfile(password
=password
)
298 del self
._passwordResetRequests
[uuid
]
299 return userid
, _('Password successfully updated.')
301 return None, _('"%s" username not found.') % userid
304 InitializeClass(RegistrationTool
)