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')
199 BaseRegistrationTool
.addMember(self
, id, password
, roles
=roles
,
200 domains
=domains
, properties
=properties
)
202 isGrpManager
= mtool
.checkPermission(ManageGroups
, portal
) ## TODO : CMF2.1 compat
203 aclu
= self
.aq_inner
.acl_users
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.'
213 aclu
.changeUser(aclu
.getGroupPrefix() +gid
, roles
=['Member', ])
214 g
= gtool
.getGroupById(gid
)
217 BaseRegistrationTool
.addMember(self
, id, password
, roles
=roles
,
218 domains
=domains
, properties
=properties
)
221 def afterAdd(self
, member
, id, password
, properties
):
222 """ notify member creation """
223 member
.notifyWorkflowCreated()
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.
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.
239 self
.clearExpiredPasswordResetRequests()
240 mtool
= getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool')
241 member
= mtool
.getMemberById(userid
)
244 checkEmailAddress(userid
)
245 member
= mtool
.searchMembers('email', userid
)
247 userid
= member
[0]['username']
248 member
= mtool
.getMemberById(userid
)
249 except EmailAddressInvalid
:
253 while self
._passwordResetRequests
.has_key(uuid
) :
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'))
266 subject
= translate(_('Complete your registration on the %s website')) % ptool
.getProperty('title')
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
,
280 ContentType
= 'text/plain',
283 mailhost
.send(message
)
286 return _('Unknown user name. Please retry.')
288 security
.declarePrivate('clearExpiredPasswordResetRequests')
289 def clearExpiredPasswordResetRequests(self
):
291 for uuid
, record
in self
._passwordResetRequests
.items() :
292 userid
, date
= record
294 del self
._passwordResetRequests
[uuid
]
297 security
.declarePublic('resetPassword')
298 def resetPassword(self
, uuid
, password
, confirm
) :
299 record
= self
._passwordResetRequests
.get(uuid
)
301 return None, _('Invalid reset password request.')
303 userid
, expiration
= record
305 if expiration
< now
:
306 self
.clearExpiredPasswordResetRequests()
307 return None, _('Your reset password request has expired. You can ask a new one.')
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
)
314 member
.setSecurityProfile(password
=password
)
315 del self
._passwordResetRequests
[uuid
]
316 return userid
, _('Password successfully updated.')
318 return None, _('"${userid}" username not found.', mapping
={'userid': userid
})
322 InitializeClass(RegistrationTool
)