# -*- coding: utf-8 -*-
## GroupUserFolder
## Copyright (C)2006 Ingeniweb

## 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; see the file COPYING. If not, write to the
## Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

## Copyright (c) 2003 The Connexions Project, All Rights Reserved
## initially written by J Cameron Cooper, 11 June 2003
## concept with Brent Hendricks, George Runyan
"""
Basic group data tool.
"""
__version__ = "$Revision:  $"
# $Source:  $
# $Id: GroupDataTool.py 30098 2006-09-08 12:35:01Z encolpe $
__docformat__ = 'restructuredtext'

from Products.CMFCore.utils import UniqueObject, getToolByName
from OFS.SimpleItem import SimpleItem
from OFS.PropertyManager import PropertyManager
from Globals import DTMLFile
from Globals import InitializeClass
from AccessControl.Role import RoleManager
from BTrees.OOBTree import OOBTree
from ZPublisher.Converters import type_converters
from Acquisition import aq_inner, aq_parent, aq_base
from AccessControl import ClassSecurityInfo, Permissions, Unauthorized, getSecurityManager

from Products.CMFCore.ActionProviderBase import ActionProviderBase
# BBB CMF < 1.5
try:
    from Products.CMFCore.permissions import ManagePortal
except ImportError:
    from Products.CMFCore.CMFCorePermissions import ManagePortal

from Products.CMFCore.MemberDataTool import CleanupTemp

from interfaces.portal_groupdata import portal_groupdata as IGroupDataTool
from interfaces.portal_groupdata import GroupData as IGroupData

_marker = []  # Create a new marker object.

from global_symbols import *


class GroupDataTool (UniqueObject, SimpleItem, PropertyManager, ActionProviderBase):
    """ This tool wraps group objects, allowing transparent access to properties.
    """
    # The latter will work only with Plone 1.1 => hence, the if
    __implements__ = (IGroupDataTool, ActionProviderBase.__implements__)

    id = 'portal_groupdata'
    meta_type = 'CMF Group Data Tool'
    _actions = ()

    _v_temps = None
    _properties=({'id':'title', 'type': 'string', 'mode': 'wd'},)

    security = ClassSecurityInfo()

    manage_options=( ActionProviderBase.manage_options +
                     ({ 'label' : 'Overview'
                       , 'action' : 'manage_overview'
                       },
                     )
                   + PropertyManager.manage_options
                   + SimpleItem.manage_options
                   )

    #
    #   ZMI methods
    #
    security.declareProtected(ManagePortal, 'manage_overview')
    manage_overview = DTMLFile('dtml/explainGroupDataTool', globals())

    def __init__(self):
        self._members = OOBTree()
        # Create the default properties.
        self._setProperty('description', '', 'text')
        self._setProperty('email', '', 'string')

    #
    #   'portal_groupdata' interface methods
    #
    security.declarePrivate('wrapGroup')
    def wrapGroup(self, g):
        """Returns an object implementing the GroupData interface"""
        id = g.getId()
        members = self._members
        if not members.has_key(id):
            # Get a temporary member that might be
            # registered later via registerMemberData().
            temps = self._v_temps
            if temps is not None and temps.has_key(id):
                portal_group = temps[id]
            else:
                base = aq_base(self)
                portal_group = GroupData(base, id)
                if temps is None:
                    self._v_temps = {id:portal_group}
                    if hasattr(self, 'REQUEST'):
                        # No REQUEST during tests.
                        self.REQUEST._hold(CleanupTemp(self))
                else:
                    temps[id] = portal_group
        else:
            portal_group = members[id]
        # Return a wrapper with self as containment and
        # the user as context.
        return portal_group.__of__(self).__of__(g)

    security.declarePrivate('registerGroupData')
    def registerGroupData(self, g, id):
        '''
        Adds the given member data to the _members dict.
        This is done as late as possible to avoid side effect
        transactions and to reduce the necessary number of
        entries.
        '''
        self._members[id] = aq_base(g)

InitializeClass(GroupDataTool)


class GroupData (SimpleItem):

    __implements__ = IGroupData

    security = ClassSecurityInfo()

    id = None
    _tool = None

    def __init__(self, tool, id):
        self.id = id
        # Make a temporary reference to the tool.
        # The reference will be removed by notifyModified().
        self._tool = tool

    def _getGRUF(self,):
        return self.acl_users

    security.declarePrivate('notifyModified')
    def notifyModified(self):
        # Links self to parent for full persistence.
        tool = getattr(self, '_tool', None)
        if tool is not None:
            del self._tool
            tool.registerGroupData(self, self.getId())

    security.declarePublic('getGroup')
    def getGroup(self):
        """ Returns the actual group implementation. Varies by group
        implementation (GRUF/Nux/et al). In GRUF this is a user object."""
        # The user object is our context, but it's possible for
        # restricted code to strip context while retaining
        # containment.  Therefore we need a simple security check.
        parent = aq_parent(self)
        bcontext = aq_base(parent)
        bcontainer = aq_base(aq_parent(aq_inner(self)))
        if bcontext is bcontainer or not hasattr(bcontext, 'getUserName'):
            raise 'GroupDataError', "Can't find group data"
        # Return the user object, which is our context.
        return parent

    def getTool(self):
        return aq_parent(aq_inner(self))

    security.declarePublic("getGroupMemberIds")
    def getGroupMemberIds(self,):
        """
        Return a list of group member ids
        """
        return map(lambda x: x.getMemberId(), self.getGroupMembers())

    security.declarePublic("getAllGroupMemberIds")
    def getAllGroupMemberIds(self,):
        """
        Return a list of group member ids
        """
        return map(lambda x: x.getMemberId(), self.getAllGroupMembers())

    security.declarePublic('getGroupMembers')
    def getGroupMembers(self, ):
        """
        Returns a list of the portal_memberdata-ish members of the group.
        This doesn't include TRANSITIVE groups/users.
        """
        md = self.portal_memberdata
        gd = self.portal_groupdata
        ret = []
        for u_name in self.getGroup().getMemberIds(transitive = 0, ):
            usr = self._getGRUF().getUserById(u_name)
            if not usr:
                raise AssertionError, "Cannot retreive a user by its id !"
            if usr.isGroup():
                ret.append(gd.wrapGroup(usr))
            else:
                ret.append(md.wrapUser(usr))
        return ret

    security.declarePublic('getAllGroupMembers')
    def getAllGroupMembers(self, ):
        """
        Returns a list of the portal_memberdata-ish members of the group.
        This will include transitive groups / users
        """
        md = self.portal_memberdata
        gd = self.portal_groupdata
        ret = []
        for u_name in self.getGroup().getMemberIds():
            usr = self._getGRUF().getUserById(u_name)
            if not usr:
                raise AssertionError, "Cannot retreive a user by its id !"
            if usr.isGroup():
                ret.append(gd.wrapGroup(usr))
            else:
                ret.append(md.wrapUser(usr))
        return ret

    def _getGroup(self,):
        """
        _getGroup(self,) => Get the underlying group object
        """
        return self._getGRUF().getGroupByName(self.getGroupName())


    security.declarePrivate("canAdministrateGroup")
    def canAdministrateGroup(self,):
        """
        Return true if the #current# user can administrate this group
        """
        user = getSecurityManager().getUser()
        tool = self.getTool()
        portal = getToolByName(tool, 'portal_url').getPortalObject()
        
        # Has manager users pemission? 
        if user.has_permission(Permissions.manage_users, portal):
            return True

        # Is explicitly mentioned as a group administrator?
        managers = self.getProperty('delegated_group_member_managers', ())
        if user.getId() in managers:
            return True

        # Belongs to a group which is explicitly mentionned as a group administrator
        meth = getattr(user, "getAllGroupNames", None)
        if meth:
            groups = meth()
        else:
            groups = ()
        for v in groups:
            if v in managers:
                return True

        # No right to edit this: we complain.
        return False

    security.declarePublic('addMember')
    def addMember(self, id):
        """ Add the existing member with the given id to the group"""
        # We check if the current user can directly or indirectly administrate this group
        if not self.canAdministrateGroup():
            raise Unauthorized, "You cannot add a member to the group."
        self._getGroup().addMember(id)

        # Notify member that they've been changed
        mtool = getToolByName(self, 'portal_membership')
        member = mtool.getMemberById(id)
        if member:
            member.notifyModified()

    security.declarePublic('removeMember')
    def removeMember(self, id):
        """Remove the member with the provided id from the group.
        """
        # We check if the current user can directly or indirectly administrate this group
        if not self.canAdministrateGroup():
            raise Unauthorized, "You cannot remove a member from the group."
        self._getGroup().removeMember(id)

        # Notify member that they've been changed
        mtool = getToolByName(self, 'portal_membership')
        member = mtool.getMemberById(id)
        if member:
            member.notifyModified()

    security.declareProtected(Permissions.manage_users, 'setProperties')
    def setProperties(self, properties=None, **kw):
        '''Allows the manager group to set his/her own properties.
        Accepts either keyword arguments or a mapping for the "properties"
        argument.
        '''
        if properties is None:
            properties = kw
        return self.setGroupProperties(properties)

    security.declareProtected(Permissions.manage_users, 'setGroupProperties')
    def setGroupProperties(self, mapping):
        '''Sets the properties of the member.
        '''
        # Sets the properties given in the MemberDataTool.
        tool = self.getTool()
        for id in tool.propertyIds():
            if mapping.has_key(id):
                if not self.__class__.__dict__.has_key(id):
                    value = mapping[id]
                    if type(value)==type(''):
                        proptype = tool.getPropertyType(id) or 'string'
                        if type_converters.has_key(proptype):
                            value = type_converters[proptype](value)
                    setattr(self, id, value)
                    
        # Hopefully we can later make notifyModified() implicit.
        self.notifyModified()

    security.declarePublic('getProperties')
    def getProperties(self, ):
        """ Return the properties of this group. Properties are as usual in Zope."""
        tool = self.getTool()
        ret = {}
        for pty in tool.propertyIds():
            try:
                ret[pty] = self.getProperty(pty)
            except ValueError:
                # We ignore missing ptys
                continue
        return ret

    security.declarePublic('getProperty')
    def getProperty(self, id, default=_marker):
        """ Returns the value of the property specified by 'id' """
        tool = self.getTool()
        base = aq_base( self )

        # First, check the wrapper (w/o acquisition).
        value = getattr( base, id, _marker )
        if value is not _marker:
            return value

        # Then, check the tool and the user object for a value.
        tool_value = tool.getProperty( id, _marker )
        user_value = getattr( aq_base(self.getGroup()), id, _marker )

        # If the tool doesn't have the property, use user_value or default
        if tool_value is _marker:
            if user_value is not _marker:
                return user_value
            elif default is not _marker:
                return default
            else:
                raise ValueError, 'The property %s does not exist' % id

        # If the tool has an empty property and we have a user_value, use it
        if not tool_value and user_value is not _marker:
            return user_value

        # Otherwise return the tool value
        return tool_value

    def __str__(self):
        return self.getGroupId()

    security.declarePublic("isGroup")
    def isGroup(self,):
        """
        isGroup(self,) => Return true if this is a group.
        Will always return true for groups.
        As MemberData objects do not support this method, it is quite useless by now.
        So one can use groupstool.isGroup(g) instead to get this information.
        """
        return 1

    ### Group object interface ###

    security.declarePublic('getGroupName')
    def getGroupName(self):
        """Return the name of the group, without any special decorations (like GRUF prefixes.)"""
        return self.getGroup().getName()

    security.declarePublic('getGroupId')
    def getGroupId(self):
        """Get the ID of the group. The ID can be used, at least from
        Python, to get the user from the user's UserDatabase.
        Within Plone, all group ids are UNPREFIXED."""
        return self.getGroup().getId(unprefixed = 1)

    def getGroupTitleOrName(self):
        """Get the Title property of the group. If there is none
        then return the name """
        title = self.getProperty('title', None)
        return title or self.getGroupName()

    security.declarePublic("getMemberId")
    def getMemberId(self,):
        """This exists only for a basic user/group API compatibility
        """
        return self.getGroupId()

    security.declarePublic('getRoles')
    def getRoles(self):
        """Return the list of roles assigned to a user."""
        return self.getGroup().getRoles()

    security.declarePublic('getRolesInContext')
    def getRolesInContext(self, object):
        """Return the list of roles assigned to the user,  including local
        roles assigned in context of the passed in object."""
        return self.getGroup().getRolesInContext(object)

    security.declarePublic('getDomains')
    def getDomains(self):
        """Return the list of domain restrictions for a user"""
        return self.getGroup().getDomains()

    security.declarePublic('has_role')
    def has_role(self, roles, object=None):
        """Check to see if a user has a given role or roles."""
        return self.getGroup().has_role(roles, object)

    # There are other parts of the interface but they are
    # deprecated for use with CMF applications.

InitializeClass(GroupData)
