X-Git-Url: https://scm.cri.minesparis.psl.eu/git/Plinn.git/blobdiff_plain/3c4367d8e03450e9a73e61f4247145d2b6c86a33..959d888c17d1403d2eeecc19bc4b5e2c8d1debf6:/Products/Plinn/RegistrationTool.py diff --git a/Products/Plinn/RegistrationTool.py b/Products/Plinn/RegistrationTool.py new file mode 100644 index 0000000..8c2911d --- /dev/null +++ b/Products/Plinn/RegistrationTool.py @@ -0,0 +1,304 @@ +# -*- coding: utf-8 -*- +####################################################################################### +# Plinn - http://plinn.org # +# © 2005-2013 Benoît PIN # +# # +# This program is free software; you can redistribute it and/or # +# modify it under the terms of the GNU General Public License # +# as published by the Free Software Foundation; either version 2 # +# of the License, or (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program; if not, write to the Free Software # +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # +####################################################################################### +""" Plinn registration tool: implements 3 modes to register members: + anonymous, manager, reviewed. + + + +""" + +from Globals import InitializeClass, PersistentMapping +from Products.PageTemplates.PageTemplateFile import PageTemplateFile +from Products.CMFDefault.RegistrationTool import RegistrationTool as BaseRegistrationTool +from AccessControl import ClassSecurityInfo, ModuleSecurityInfo +from AccessControl.Permission import Permission +from BTrees.OOBTree import OOBTree +from Products.CMFCore.permissions import ManagePortal, AddPortalMember +from Products.CMFCore.exceptions import AccessControl_Unauthorized +from Products.CMFDefault.exceptions import EmailAddressInvalid +from Products.CMFCore.utils import getToolByName +from Products.CMFCore.utils import getUtilityByInterfaceName +from Products.CMFDefault.utils import checkEmailAddress +from Products.GroupUserFolder.GroupsToolPermissions import ManageGroups +from Products.Plinn.utils import Message as _ +from Products.Plinn.utils import translate +from Products.Plinn.utils import encodeQuopriEmail +from Products.Plinn.utils import encodeMailHeader +from DateTime import DateTime +from types import TupleType, ListType +from uuid import uuid4 + +security = ModuleSecurityInfo('Products.Plinn.RegistrationTool') +MODE_ANONYMOUS = 'anonymous' +security.declarePublic('MODE_ANONYMOUS') + +MODE_PASS_ANONYMOUS = 'pass_anonymous' +security.declarePublic('MODE_PASS_ANONYMOUS') + +MODE_MANAGER = 'manager' +security.declarePublic('MODE_MANAGER') + +MODE_REVIEWED = 'reviewed' +security.declarePublic('MODE_REVIEWED') + +MODES = [MODE_ANONYMOUS, MODE_PASS_ANONYMOUS, MODE_MANAGER, MODE_REVIEWED] +security.declarePublic('MODES') + +DEFAULT_MEMBER_GROUP = 'members' +security.declarePublic('DEFAULT_MEMBER_GROUP') + + + +class RegistrationTool(BaseRegistrationTool) : + + """ Create and modify users by making calls to portal_membership. + """ + + meta_type = "Plinn Registration Tool" + + manage_options = ({'label' : 'Registration mode', 'action' : 'manage_regmode'}, ) + \ + BaseRegistrationTool.manage_options + + security = ClassSecurityInfo() + + security.declareProtected( ManagePortal, 'manage_regmode' ) + manage_regmode = PageTemplateFile('www/configureRegistrationTool', globals(), + __name__='manage_regmode') + + def __init__(self) : + self._mode = MODE_ANONYMOUS + self._chain = '' + self._passwordResetRequests = OOBTree() + + security.declareProtected(ManagePortal, 'configureTool') + def configureTool(self, registration_mode, chain, REQUEST=None) : + """ """ + + if registration_mode not in MODES : + raise ValueError, "Unknown mode: " + registration_mode + else : + self._mode = registration_mode + self._updatePortalRoleMappingForMode(registration_mode) + + wtool = getToolByName(self, 'portal_workflow') + + if registration_mode == MODE_REVIEWED : + if not hasattr(wtool, '_chains_by_type') : + wtool._chains_by_type = PersistentMapping() + wfids = [] + chain = chain.strip() + + if chain == '(Default)' : + try : del wtool._chains_by_type['Member Data'] + except KeyError : pass + self._chain = chain + else : + for wfid in chain.replace(',', ' ').split(' ') : + if wfid : + if not wtool.getWorkflowById(wfid) : + raise ValueError, '"%s" is not a workflow ID.' % wfid + wfids.append(wfid) + + wtool._chains_by_type['Member Data'] = tuple(wfids) + self._chain = ', '.join(wfids) + else : + wtool._chains_by_type['Member Data'] = tuple() + + if REQUEST : + REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_regmode?manage_tabs_message=Saved changes.') + + def _updatePortalRoleMappingForMode(self, mode) : + + urlTool = getToolByName(self, 'portal_url') + portal = urlTool.getPortalObject() + + if mode in [MODE_ANONYMOUS, MODE_PASS_ANONYMOUS, MODE_REVIEWED] : + portal.manage_permission(AddPortalMember, roles = ['Anonymous', 'Manager'], acquire=1) + elif mode == MODE_MANAGER : + portal.manage_permission(AddPortalMember, roles = ['Manager', 'UserManager'], acquire=0) + + security.declarePublic('getMode') + def getMode(self) : + # """ return current mode """ + return self._mode[:] + + security.declarePublic('getWfId') + def getWfChain(self) : + # """ return current workflow id """ + return self._chain + + security.declarePublic('roleMappingMismatch') + def roleMappingMismatch(self) : + # """ test if the role mapping is correct for the currrent mode """ + + mode = self._mode + urlTool = getToolByName(self, 'portal_url') + portal = urlTool.getPortalObject() + + def rolesOfAddPortalMemberPerm() : + p=Permission(AddPortalMember, [], portal) + return p.getRoles() + + if mode in [MODE_ANONYMOUS, MODE_PASS_ANONYMOUS, MODE_REVIEWED] : + if 'Anonymous' in rolesOfAddPortalMemberPerm() : return False + + elif mode == MODE_MANAGER : + roles = rolesOfAddPortalMemberPerm() + if 'Manager' in roles or 'UserManager' in roles and len(roles) == 1 and type(roles) == TupleType : + return False + + return True + + security.declareProtected(AddPortalMember, 'addMember') + def addMember(self, id, password, roles=(), groups=(DEFAULT_MEMBER_GROUP,), domains='', properties=None) : + """ Idem CMFCore but without default role """ + + if self.getMode() != MODE_REVIEWED : + gtool = getToolByName(self, 'portal_groups') + mtool = getToolByName(self, 'portal_membership') + utool = getToolByName(self, 'portal_url') + portal = utool.getPortalObject() + + if self.getMode() == MODE_PASS_ANONYMOUS : + private_collections = portal.get('private_collections') + if not private_collections : + raise AccessControl_Unauthorized() + return + data = private_collections.data + lines = filter(None, [l.strip() for l in data.split('\n')]) + assert len(lines) % 3 == 0 + collecInfos = {} + for i in xrange(0, len(lines), 3) : + collecInfos[lines[i]] = {'pw' : lines[i+1], + 'path' : lines[i+2]} + if not (collecInfos.has_key(properties.get('collection_id')) and \ + collecInfos[properties.get('collection_id')]['pw'] == properties.get('collection_password')) : + raise AccessControl_Unauthorized('Wrong primary credentials') + return + + + BaseRegistrationTool.addMember(self, id, password, roles=roles, + domains=domains, properties=properties) + + isGrpManager = mtool.checkPermission(ManageGroups, portal) ## TODO : CMF2.1 compat + aclu = self.aq_inner.acl_users + + for gid in groups: + g = gtool.getGroupById(gid) + if not isGrpManager : + if gid != DEFAULT_MEMBER_GROUP: + raise AccessControl_Unauthorized, 'You are not allowed to join arbitrary group.' + + if g is None : + gtool.addGroup(gid) + aclu.changeUser(aclu.getGroupPrefix() +gid, roles=['Member', ]) + g = gtool.getGroupById(gid) + g.addMember(id) + else : + BaseRegistrationTool.addMember(self, id, password, roles=roles, + domains=domains, properties=properties) + + + def afterAdd(self, member, id, password, properties): + """ notify member creation """ + member.notifyWorkflowCreated() + member.indexObject() + + + security.declarePublic('requestPasswordReset') + def requestPasswordReset(self, userid): + """ add uuid / (userid, expiration) pair and return uuid """ + self.clearExpiredPasswordResetRequests() + mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool') + member = mtool.getMemberById(userid) + if not member : + try : + checkEmailAddress(userid) + member = mtool.searchMembers('email', userid) + if member : + userid = member[0]['username'] + member = mtool.getMemberById(userid) + except EmailAddressInvalid : + pass + if member : + uuid = str(uuid4()) + while self._passwordResetRequests.has_key(uuid) : + uuid = str(uuid4()) + self._passwordResetRequests[uuid] = (userid, DateTime() + 1) + utool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IURLTool') + ptool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IPropertiesTool') + # fuck : mailhost récupéré avec getUtilityByInterfaceName n'est pas correctement + # wrappé. Un « unrestrictedTraverse » ne marche pas. + # mailhost = getUtilityByInterfaceName('Products.MailHost.interfaces.IMailHost') + portal = utool.getPortalObject() + mailhost = portal.MailHost + sender = encodeQuopriEmail(ptool.getProperty('email_from_name'), ptool.getProperty('email_from_address')) + to = encodeQuopriEmail(member.getMemberFullName(nameBefore=0), member.getProperty('email')) + subject = translate(_('How to reset your password on the %s website')) % ptool.getProperty('title') + subject = encodeMailHeader(subject) + options = {'fullName' : member.getMemberFullName(nameBefore=0), + 'siteName' : ptool.getProperty('title'), + 'resetPasswordUrl' : '%s/password_reset_form/%s' % (utool(), uuid)} + body = self.password_reset_mail(options) + message = self.echange_mail_template(From=sender, + To=to, + Subject=subject, + ContentType = 'text/plain', + charset = 'UTF-8', + body=body) + mailhost.send(message) + return + + return _('Unknown user name. Please retry.') + + security.declarePrivate('clearExpiredPasswordResetRequests') + def clearExpiredPasswordResetRequests(self): + now = DateTime() + for uuid, record in self._passwordResetRequests.items() : + userid, date = record + if date < now : + del self._passwordResetRequests[uuid] + + + security.declarePublic('resetPassword') + def resetPassword(self, uuid, password, confirm) : + record = self._passwordResetRequests.get(uuid) + if not record : + return None, _('Invalid reset password request.') + + userid, expiration = record + now = DateTime() + if expiration < now : + self.clearExpiredPasswordResetRequests() + return None, _('Your reset password request has expired. You can ask a new one.') + + msg = self.testPasswordValidity(password, confirm=confirm) + if not msg : # None if everything ok. Err message otherwise. + mtool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IMembershipTool') + member = mtool.getMemberById(userid) + if member : + member.setSecurityProfile(password=password) + del self._passwordResetRequests[uuid] + return userid, _('Password successfully updated.') + else : + return None, _('"%s" username not found.') % userid + + +InitializeClass(RegistrationTool) \ No newline at end of file