import PIL from cStringIO import StringIO from DateTime import DateTime from Products.CMFCore.utils import getToolByName, _checkPermission from Products.CMFDefault.MembershipTool import MembershipTool as BaseTool from Products.CMFPlone import ToolNames from Products.CMFPlone.utils import scale_image from Products.CMFPlone.utils import _createObjectByType from OFS.Image import Image from AccessControl import ClassSecurityInfo, getSecurityManager from Globals import InitializeClass, DTMLFile from zExceptions import BadRequest from ZODB.POSException import ConflictError from AccessControl.SecurityManagement import noSecurityManager from Acquisition import aq_base, aq_parent, aq_inner from Products.CMFCore.permissions import ManagePortal from Products.CMFCore.permissions import ManageUsers from Products.CMFCore.permissions import SetOwnProperties from Products.CMFCore.permissions import SetOwnPassword from Products.CMFCore.permissions import View from Products.CMFPlone.PloneBaseTool import PloneBaseTool from Products.CMFPlone.log import log from Products.CMFPlone.utils import postonly default_portrait = 'defaultUser.gif' _marker = object() class MembershipTool(PloneBaseTool, BaseTool): meta_type = ToolNames.MembershipTool toolicon = 'skins/plone_images/user.gif' plone_tool = 1 personal_id = '.personal' portrait_id = 'MyPortrait' default_portrait = 'defaultUser.gif' memberarea_type = 'Folder' security = ClassSecurityInfo() __implements__ = (PloneBaseTool.__implements__, BaseTool.__implements__, ) manage_options = (BaseTool.manage_options + ( { 'label' : 'Portraits' , 'action' : 'manage_portrait_fix' },)) # TODO I'm not quite sure why getPortalRoles is declared 'Managed' # in CMFCore.MembershipTool - but in Plone we are not so anal ;-) security.declareProtected(View, 'getPortalRoles') security.declareProtected(ManagePortal, 'manage_mapRoles') manage_mapRoles = DTMLFile('www/membershipRolemapping', globals()) security.declareProtected(ManagePortal, 'manage_portrait_fix') manage_portrait_fix = DTMLFile('www/portrait_fix', globals()) security.declareProtected(ManagePortal, 'manage_setMemberAreaType') def manage_setMemberAreaType(self, type_name, REQUEST=None): """ ZMI method to set the home folder type by its type name. """ self.setMemberAreaType(type_name) if REQUEST is not None: REQUEST['RESPONSE'].redirect(self.absolute_url() + '/manage_mapRoles' + '?manage_tabs_message=Member+area+type+changed.') security.declareProtected(ManagePortal, 'setMemberAreaType') def setMemberAreaType(self, type_name): """ Sets the portal type to use for new home folders. """ # No check for folderish since someone somewhere may actually want # members to have objects instead of folders as home "directory". self.memberarea_type = str(type_name).strip() security.declarePublic('getMemberInfo') def getMemberInfo(self, memberId=None): """ Return 'harmless' Memberinfo of any member, such as Full name, Location, etc """ if not memberId: member = self.getAuthenticatedMember() else: member = self.getMemberById(memberId) if member is None: return None memberinfo = { 'fullname' : member.getProperty('fullname'), 'description' : member.getProperty('description'), 'location' : member.getProperty('location'), 'language' : member.getProperty('language'), 'home_page' : member.getProperty('home_page'), 'username' : member.getUserName(), } return memberinfo security.declarePublic('getPersonalPortrait') def getPersonalPortrait(self, member_id = None, verifyPermission=0): """ returns the Portrait for a member_id """ membertool = getToolByName(self, 'portal_memberdata') if not member_id: member_id = self.getAuthenticatedMember().getId() portrait = membertool._getPortrait(member_id) if type(portrait) == type(''): portrait = None if portrait is not None: if verifyPermission and not _checkPermission('View', portrait): # Don't return the portrait if the user can't get to it portrait = None if portrait is None: portal = getToolByName(self, 'portal_url').getPortalObject() portrait = getattr(portal, default_portrait) return portrait security.declareProtected(SetOwnProperties, 'deletePersonalPortrait') def deletePersonalPortrait(self, member_id = None): """ deletes the Portrait of member_id """ membertool = getToolByName(self, 'portal_memberdata') if not member_id: member_id = self.getAuthenticatedMember().getId() membertool._deletePortrait(member_id) security.declarePublic('getHomeFolder') def getHomeFolder(self, id=None, verifyPermission=0): """ Return a member's home folder object, or None. dwm: straight from CMF1.5.2 """ if id is None: member = self.getAuthenticatedMember() if not hasattr(member, 'getMemberId'): return None id = member.getMemberId() members = self.getMembersFolder() if members: try: folder = members._getOb(id) if verifyPermission and not _checkPermission(View, folder): # Don't return the folder if the user can't get to it. return None return folder # KeyError added to deal with btree member folders except (AttributeError, KeyError, TypeError): pass return None security.declarePublic('getPersonalFolder') def getPersonalFolder(self, member_id=None): """ returns the Personal Item folder for a member if no Personal Folder exists will return None """ home=self.getHomeFolder(member_id) personal=None if home: personal=getattr( home , self.personal_id , None ) return personal security.declareProtected(SetOwnProperties, 'changeMemberPortrait') def changeMemberPortrait(self, portrait, member_id=None): """ given a portrait we will modify the users portrait we put this method here because we do not want .personal or portrait in the catalog """ if not member_id: member_id = self.getAuthenticatedMember().getId() if portrait and portrait.filename: scaled, mimetype = scale_image(portrait) portrait = Image(id=member_id, file=scaled, title='') membertool = getToolByName(self, 'portal_memberdata') membertool._setPortrait(portrait, member_id) security.declarePublic('createMemberarea') def createMemberarea(self, member_id=None, minimal=1): """ Create a member area for 'member_id' or the authenticated user. """ if not self.getMemberareaCreationFlag(): return None members = self.getMembersFolder() if members is None: return None if self.isAnonymousUser(): return None catalog = getToolByName(self, 'portal_catalog') membership = getToolByName(self, 'portal_membership') if not member_id: # member_id is optional (see CMFCore.interfaces.portal_membership: # Create a member area for 'member_id' or authenticated user.) member = membership.getAuthenticatedMember() member_id = member.getId() if hasattr(members, 'aq_explicit'): members=members.aq_explicit if hasattr(members, member_id): # has already this member # TODO exception return _createObjectByType(self.memberarea_type, members, id=member_id) # get the user object from acl_users # TODO what about portal_membership.getAuthenticatedMember()? acl_users = self.__getPUS() user = acl_users.getUser(member_id) if user is not None: user= user.__of__(acl_users) else: user= getSecurityManager().getUser() # check that we do not do something wrong if user.getId() != member_id: raise NotImplementedError, \ 'cannot get user for member area creation' ## translate the default content translation_service = getToolByName(self, 'translation_service', _marker) if translation_service is _marker: # test environ, some other aberent sitch return utranslate = translation_service.utranslate encode = translation_service.encode # convert the member_id to unicode type umember_id = translation_service.asunicodetype(user.getUserName(), errors='replace') member_folder_title = utranslate( 'plone', 'title_member_folder', {'member': umember_id}, self, default = "%s" % umember_id) member_folder_description = utranslate( 'plone', 'description_member_folder', {'member': umember_id}, self, default = '') member_folder_index_html_title = utranslate( 'plone', 'title_member_folder_index_html', {'member': umember_id}, self, default = "Home page for %s" % umember_id) # encode strings to site encoding as we dont like to store type unicode atm member_folder_title = encode(member_folder_title, errors='replace') member_folder_description = encode(member_folder_description, errors='replace') member_folder_index_html_title = encode(member_folder_index_html_title, errors='replace') ## Modify member folder member_folder = self.getHomeFolder(member_id) # Grant Ownership and Owner role to Member member_folder.changeOwnership(user) member_folder.__ac_local_roles__ = None member_folder.manage_setLocalRoles(member_id, ['Owner']) # We use ATCT now use the mutators member_folder.setTitle(member_folder_title) member_folder.setDescription(member_folder_description) member_folder.reindexObject() if not minimal: # if it's minimal, don't create the memberarea but do notification ## add homepage text # get the text from portal_skins automagically homepageText = getattr(self, 'homePageText', None) if homepageText: member_object = self.getMemberById(member_id) portal = getToolByName(self, 'portal_url') # call the page template content = homepageText(member=member_object, portal=portal).strip() _createObjectByType('Document', member_folder, id='index_html') hpt = getattr(member_folder, 'index_html') # edit title, text and format # TODO hpt.setTitle(member_folder_index_html_title) if hpt.meta_type == 'Document': # CMFDefault Document hpt.edit(text_format='structured-text', text=content) else: hpt.update(text=content) hpt.setFormat('structured-text') hpt.reindexObject() # Grant Ownership and Owner role to Member hpt.changeOwnership(user) hpt.__ac_local_roles__ = None hpt.manage_setLocalRoles(member_id, ['Owner']) ## Hook to allow doing other things after memberarea creation. notify_script = getattr(member_folder, 'notifyMemberAreaCreated', None) if notify_script is not None: notify_script() # deal with ridiculous API change in CMF security.declarePublic('createMemberArea') createMemberArea = createMemberarea security.declareProtected(ManageUsers, 'listMembers') def listMembers(self): '''Gets the list of all members. THIS METHOD MIGHT BE VERY EXPENSIVE ON LARGE USER FOLDERS AND MUST BE USED WITH CARE! We plan to restrict its use in the future (ie. force large requests to use searchForMembers instead of listMembers, so that it will not be possible anymore to have a method returning several hundred of users :) ''' uf = self.acl_users if hasattr(aq_base(uf), 'getPureUsers'): # GRUF return [BaseTool.wrapUser(self, x) for x in uf.getPureUsers()] else: return BaseTool.listMembers(self) security.declareProtected(ManageUsers, 'listMemberIds') def listMemberIds(self): '''Lists the ids of all members. This may eventually be replaced with a set of methods for querying pieces of the list rather than the entire list at once. ''' uf = self.acl_users if hasattr(aq_base(uf), 'getPureUserIds'): # GRUF return uf.getPureUserIds() else: return self.__getPUS().getUserIds() # this should probably be in MemberDataTool.py security.declarePublic('searchForMembers') def searchForMembers( self, REQUEST=None, **kw ): """ searchForMembers(self, REQUEST=None, **kw) => normal or fast search method. The following properties can be provided: - name - email - last_login_time - roles - groupname This is an 'AND' request. If name is provided, then a _fast_ search is performed with GRUF's searchUsersByName() method. This will improve performance. In any other case, a regular (possibly _slow_) search is performed. As it uses the listMembers() method, which is itself based on gruf.getUsers(), this can return partial results. This may change in the future. """ md = self.portal_memberdata groups_tool = self.portal_groups if REQUEST: dict = REQUEST else: dict = kw name = dict.get('name', None) email = dict.get('email', None) roles = dict.get('roles', None) last_login_time = dict.get('last_login_time', None) before_specified_time = dict.get('before_specified_time', None) groupname = dict.get('groupname', '').strip() is_manager = self.checkPermission('Manage portal', self) if name: name = name.strip().lower() if not name: name = None if email: email = email.strip().lower() if not email: email = None # We want 'name' request to be handled properly with large user folders. # So we have to check both the fullname and loginname, without scanning all # possible users. md_users = None uf_users = None if name: # We first find in MemberDataTool users whose _full_ name match what we want. lst = md.searchMemberDataContents('fullname', name) md_users = [ x['username'] for x in lst] # Fast search management if the underlying acl_users support it. # This will allow us to retreive users by their _id_ (not name). acl_users = self.acl_users meth = getattr(acl_users, "searchUsersByName", None) if meth: uf_users = meth(name) # gruf search # Now we have to merge both lists to get a nice users set. # This is possible only if both lists are filled (or we may miss users else). members = [] g_userids, g_members = [], [] if groupname: groups = groups_tool.searchForGroups(title=groupname) + \ groups_tool.searchForGroups(name=groupname) for group in groups: for member in group.getGroupMembers(): if member not in g_members and not groups_tool.isGroup(member): g_members.append(member) g_userids = map(lambda x: x.getMemberId(), g_members) if groupname and not g_userids: return [] if md_users is not None and uf_users is not None: names_checked = 1 wrap = self.wrapUser getUser = acl_users.getUser for userid in md_users: if not g_userids or userid in g_userids: members.append(wrap(getUser(userid))) for userid in uf_users: if userid in md_users: continue # Kill dupes if not g_userids or userid in g_userids: members.append(wrap(getUser(userid))) # Optimization trick if not email and \ not roles and \ not last_login_time: return members elif groupname: members = g_members names_checked = 0 else: # If the lists are not available, we just stupidly get the members list members = self.listMembers() names_checked = 0 # Now perform individual checks on each user res = [] portal = getToolByName(self, 'portal_url').getPortalObject() for member in members: #user = md.wrapUser(u) if not (member.getProperty('listed', None) or is_manager): continue if name and not names_checked: if (member.getUserName().lower().find(name) == -1 and member.getProperty('fullname').lower().find(name) == -1): continue if email: if member.getProperty('email').lower().find(email) == -1: continue if roles: user_roles = member.getRoles() found = 0 for r in roles: if r in user_roles: found = 1 break if not found: continue if last_login_time: if type(member.getProperty('last_login_time','')) == type(''): # value is a string when mem hasn't yet logged in mem_last_login_time = DateTime(member.getProperty('last_login_time','2000/01/01')) else: mem_last_login_time = member.getProperty('last_login_time') if before_specified_time: if mem_last_login_time >= last_login_time: continue elif mem_last_login_time < last_login_time: continue res.append(member) return res security.declareProtected(SetOwnPassword, 'testCurrentPassword') def testCurrentPassword(self, password): """ test to see if password is current """ REQUEST=getattr(self, 'REQUEST', {}) userid=self.getAuthenticatedMember().getUserId() acl_users = self._findUsersAclHome(userid) if not acl_users: return 0 return acl_users.authenticate(userid, password, REQUEST) def _findUsersAclHome(self, userid): portal = getToolByName(self, 'portal_url').getPortalObject() acl_users=portal.acl_users parent = acl_users while parent: if acl_users.aq_explicit.getUserById(userid, None) is not None: break parent = aq_parent(aq_inner(parent)).aq_parent acl_users = getattr(parent, 'acl_users') if parent: return acl_users else: return None security.declareProtected(SetOwnPassword, 'setPassword') def setPassword(self, password, domains=None, REQUEST=None): '''Allows the authenticated member to set his/her own password. ''' registration = getToolByName(self, 'portal_registration', None) if not self.isAnonymousUser(): member = self.getAuthenticatedMember() acl_users = self._findUsersAclHome(member.getUserId())#self.acl_users if not acl_users: # should not possibly ever happen raise BadRequest, 'did not find current user in any user folder' if registration: failMessage = registration.testPasswordValidity(password) if failMessage is not None: raise BadRequest, failMessage if domains is None: domains = [] user = acl_users.getUserById(member.getUserId(), None) # we must change the users password trough grufs changepassword # to keep her group settings if hasattr(user, 'changePassword'): user.changePassword(password) else: acl_users._doChangeUser(member.getUserId(), password, member.getRoles(), domains) self.credentialsChanged(password) else: raise BadRequest, 'Not logged in.' setPassword = postonly(setPassword) security.declareProtected(View, 'getCandidateLocalRoles') def getCandidateLocalRoles(self, obj): """ What local roles can I assign? Override the CMFCore version so that we can see the local roles on an object, and so that local managers can assign all roles locally. """ member = self.getAuthenticatedMember() # Use getRolesInContext as someone may be a local manager if 'Manager' in member.getRolesInContext(obj): # Use valid_roles as we may want roles defined only on a subobject local_roles = [r for r in obj.valid_roles() if r not in ('Anonymous', 'Authenticated', 'Shared')] else: local_roles = [ role for role in member.getRolesInContext(obj) if role not in ('Member', 'Authenticated') ] local_roles.sort() return tuple(local_roles) security.declareProtected(View, 'immediateLogout') def immediateLogout(self): """ Log the current user out immediately. Used by logout.py so that we do not have to do a redirect to show the logged out status. """ noSecurityManager() security.declarePublic('setLoginTimes') def setLoginTimes(self): """ Called by logged_in to set the login time properties even if members lack the "Set own properties" permission. """ if not self.isAnonymousUser(): member = self.getAuthenticatedMember() login_time = member.getProperty('login_time', '2000/01/01') if str(login_time) == '2000/01/01': login_time = self.ZopeTime() member.setProperties(login_time=self.ZopeTime(), last_login_time=login_time) security.declareProtected(ManagePortal, 'getBadMembers') def getBadMembers(self): """Will search for members with bad images in the portal_memberdata delete their portraits and return their member ids""" memberdata = getToolByName(self, 'portal_memberdata') portraits = getattr(memberdata, 'portraits', None) if portraits is None: return [] bad_member_ids = [] import transaction TXN_THRESHOLD = 50 counter = 1 for member_id in tuple(portraits.objectIds()): portrait = portraits[member_id] portrait_data = str(portrait.data) if portrait_data == '': continue try: img = PIL.Image.open(StringIO(portrait_data)) except ConflictError: pass except: # Anything else we have a bad bad image and we destroy it # and ask questions later. portraits._delObject(member_id) bad_member_ids.append(member_id) if not counter%TXN_THRESHOLD: transaction.savepoint(optimistic=True) counter = counter + 1 return bad_member_ids MembershipTool.__doc__ = BaseTool.__doc__ InitializeClass(MembershipTool)