##############################################################################
#
# Copyright (c) 2001 Zope Foundation and Contributors
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this
# distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
""" Classes:  PropertiedUser

$Id$
"""

from Acquisition import aq_inner, aq_parent
from AccessControl.User import BasicUser
from AccessControl.PermissionRole import _what_not_even_god_should_do

from interfaces.authservice import IPropertiedUser
from UserPropertySheet import UserPropertySheet
from utils import classImplements
from Products.PluggableAuthService.interfaces.propertysheets \
    import IPropertySheet

class PropertiedUser( BasicUser ):

    """ User objects which manage propertysheets, obtained from decorators.
    """
    def __init__( self, id, login=None ):

        self._id = id

        if login is None:
            login = id

        self._login = login

        self._propertysheets = {}   # *Not* persistent!
        self._groups = {}
        self._roles = {}


    #
    #   BasicUser's public interface
    #
    def getId( self ):

        """ -> user ID
        """
        return self._id

    def getUserName( self ):

        """ -> login name
        """
        return self._login

    def getRoles( self ):

        """ -> [ role ]

        o Include only "global" roles.
        """
        return self._roles.keys()

    def getGroups(self):
        """ -> [group]

        o Return the groups the user is in.
        """
        return self._groups.keys()

    def getDomains( self ):

        """ -> [ domain ]

        o The list represents the only domains from which the user is
          allowed to access the system.
        """
        return ()

    def getRolesInContext( self, object ):

        """ Return the list of roles assigned to the user.

        o Include local roles assigned in context of the passed-in object.

        o Include *both* local roles assigned directly to us *and* those
          assigned to our groups.

        o Ripped off from AccessControl.User.BasicUser, which provides
          no other extension mechanism. :(
        """
        user_id = self.getId()
        # [ x.getId() for x in self.getGroups() ]
        group_ids = self.getGroups()

        principal_ids = list( group_ids )
        principal_ids.insert( 0, user_id )

        local ={}
        object = aq_inner( object )

        while 1:

            local_roles = getattr( object, '__ac_local_roles__', None )

            if local_roles:

                if callable( local_roles ):
                    local_roles = local_roles()

                dict = local_roles or {}

                for principal_id in principal_ids:
                    for role in dict.get( principal_id, [] ):
                        local[ role ] = 1

            inner = aq_inner( object )
            parent = aq_parent( inner )

            if parent is not None:
                object = parent
                continue

            new = getattr( object, 'im_self', None )

            if new is not None:

                object = aq_inner( new )
                continue

            break

        return list( self.getRoles() ) + local.keys()

    def allowed( self, object, object_roles=None ):

        """ Check whether the user has access to object.

        o The user must have one of the roles in object_roles to allow access.

        o Include *both* local roles assigned directly to us *and* those
          assigned to our groups.

        o Ripped off from AccessControl.User.BasicUser, which provides
          no other extension mechanism. :(
        """
        if object_roles is _what_not_even_god_should_do:
            return 0

        # Short-circuit the common case of anonymous access.
        if object_roles is None or 'Anonymous' in object_roles:
            return 1

        # Provide short-cut access if object is protected by 'Authenticated'
        # role and user is not nobody
        if 'Authenticated' in object_roles and (
            self.getUserName() != 'Anonymous User'):
            return 1

        # Check for ancient role data up front, convert if found.
        # This should almost never happen, and should probably be
        # deprecated at some point.
        if 'Shared' in object_roles:
            object_roles = self._shared_roles(object)
            if object_roles is None or 'Anonymous' in object_roles:
                return 1

        # Check for a role match with the normal roles given to
        # the user, then with local roles only if necessary. We
        # want to avoid as much overhead as possible.
        user_roles = self.getRoles()
        for role in object_roles:
            if role in user_roles:
                if self._check_context(object):
                    return 1
                return None

        # Still have not found a match, so check local roles. We do
        # this manually rather than call getRolesInContext so that
        # we can incur only the overhead required to find a match.
        inner_obj = aq_inner( object )
        user_id = self.getId()
        # [ x.getId() for x in self.getGroups() ]
        group_ids = self.getGroups()

        principal_ids = list( group_ids )
        principal_ids.insert( 0, user_id )

        while 1:

            local_roles = getattr( inner_obj, '__ac_local_roles__', None )

            if local_roles:

                if callable( local_roles ):
                    local_roles = local_roles()

                dict = local_roles or {}

                for principal_id in principal_ids:

                    local_roles = dict.get( principal_id, [] )

                    for role in object_roles:

                        if role in local_roles:

                            if self._check_context( object ):
                                return 1

                            return 0

            inner = aq_inner( inner_obj )
            parent = aq_parent( inner )

            if parent is not None:
                inner_obj = parent
                continue

            new = getattr( inner_obj, 'im_self', None )

            if new is not None:
                inner_obj = aq_inner( new )
                continue

            break

        return None

    #
    #   Interfaces to allow user folder plugins to annotate the user.
    #
    def _addGroups( self, groups=() ):

        """ Extend our set of groups.

        o Don't complain about duplicates.
        """
        for group in groups:
            self._groups[ group ] = 1

    def _addRoles( self, roles=() ):

        """ Extend our set of roles.

        o Don't complain about duplicates.
        """
        for role in roles:
            self._roles[ role ] = 1


    #
    #   Propertysheet management
    #
    def listPropertysheets( self ):

        """ -> [ propertysheet_id ]
        """
        return self._propertysheets.keys()

    def getPropertysheet( self, id ):

        """ id -> sheet

        o Raise KeyError if no such seet exists.
        """
        return self._propertysheets[ id ]

    __getitem__ = getPropertysheet

    def addPropertysheet( self, id, data ):

        """ Add a new propertysheet.

        o Raise KeyError if a sheet of the given ID already exists.
        """
        if self._propertysheets.get( id ) is not None:
            raise KeyError, "Duplicate property sheet: %s" % id

        if IPropertySheet.providedBy(data):
            self._propertysheets[ id ] = data
        else:
            self._propertysheets[ id ] = UserPropertySheet( id, **data )


classImplements( PropertiedUser,
                 IPropertiedUser )
