##############################################################################
#
# 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: DynamicGroupsPlugin

$Id$
"""
import copy

from Acquisition import aq_inner, aq_parent
from AccessControl import ClassSecurityInfo
from AccessControl.requestmethod import postonly
from OFS.SimpleItem import SimpleItem
from OFS.PropertyManager import PropertyManager
from OFS.Folder import Folder
from OFS.Cache import Cacheable
from App.class_init import InitializeClass

from zope.interface import Interface

from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from Products.PageTemplates.Expressions import getEngine

from Products.PluggableAuthService.interfaces.plugins \
    import IGroupsPlugin
from Products.PluggableAuthService.interfaces.plugins \
    import IGroupEnumerationPlugin
from Products.PluggableAuthService.permissions import ManageGroups
from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin
from Products.PluggableAuthService.utils import createViewName
from Products.PluggableAuthService.utils import classImplements
from Products.PluggableAuthService.utils import csrf_only


class IDynamicGroupsPlugin(Interface):
    """ Marker interface.
    """


manage_addDynamicGroupsPluginForm = PageTemplateFile(
        'www/dgpAdd', globals(), __name__= 'manage_addDynamicGroupsPluginForm' )

def addDynamicGroupsPlugin( dispatcher, id, title='', RESPONSE=None ):

    """ Add a DGP to 'dispatcher'.
    """
    dgp = DynamicGroupsPlugin( id, title )
    dispatcher._setObject( id, dgp )

    if RESPONSE is not None:
        RESPONSE.redirect( '%s/manage_main?manage_tabs_messsage=%s'
                         % ( dispatcher.absolute_url()
                           , 'DPG+added.'
                           )
                         )

class DynamicGroupDefinition( SimpleItem, PropertyManager ):

    """ Represent a single dynamic group.
    """
    meta_type = 'Dynamic Group Definition'
    security = ClassSecurityInfo()
    security.declareObjectProtected( ManageGroups )

    _v_compiled = None


    _properties = ( { 'id' : 'id'
                    , 'type' : 'string'
                    , 'mode' : ''
                    }
                  , { 'id' : 'predicate'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id' : 'title'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id' : 'description'
                    , 'type' : 'text'
                    , 'mode' : 'w'
                    }
                  , { 'id' : 'active'
                    , 'type' : 'boolean'
                    , 'mode' : 'w'
                    }
                  )

    def __init__( self, id, predicate, title, description, active ):

        self._setId( id )
        self._setPredicate( predicate )

        self.title = title
        self.description = description
        self.active = bool( active )

    def __call__( self, principal, request=None ):

        """ Evaluate our expression to determine whether 'principal' belongs.
        """
        predicate = self._getPredicate()
        plugin = aq_parent( aq_inner( self ) )
        properties = {}

        for k, v in self.propertyItems():
            properties[ k ] = v

        data = getEngine().getContext( { 'request' :    request
                                       , 'nothing' :    None
                                       , 'principal' :  principal
                                       , 'group' :      properties
                                       , 'plugin' :     plugin
                                       } )

        result = predicate( data )

        if isinstance( result, Exception ):
            raise result

        return result

    security.declarePrivate( '_setPredicate' )
    def _setPredicate( self, predicate ):

        self.predicate = predicate

        if self._v_compiled is not None:
            del self._v_compiled

    security.declarePrivate( '_getPredicate' )
    def _getPredicate( self ):

        if self._v_compiled is None:
            self._v_compiled = getEngine().compile( self.predicate )

        return self._v_compiled

    security.declarePrivate( '_updateProperty' )
    def _updateProperty( self, id, value ):

        if id == 'predicate':
            self._setPredicate( value )

        else:
            PropertyManager._updateProperty( self, id, value )

    #
    #   ZMI
    #
    manage_options = ( PropertyManager.manage_options
                     + SimpleItem.manage_options
                     )

InitializeClass( DynamicGroupDefinition )


class DynamicGroupsPlugin( Folder, BasePlugin, Cacheable ):

    """ Define groups via business rules.

    o Membership in a candidate group is established via a predicate,
      expressed as a TALES expression.  Names available to the predicate
      include:

      'group' -- the dynamic group definition object itself

      'plugin' -- this plugin object

      'principal' -- the principal being tested.

      'request' -- the request object.
    """
    meta_type = 'Dynamic Groups Plugin'

    security = ClassSecurityInfo()

    def __init__( self, id, title='' ):

        self._setId( id )
        self.title = title

    #
    #   Plugin implementations
    #
    security.declareProtected( ManageGroups, 'getGroupsForPrincipal' )
    def getGroupsForPrincipal( self, principal, request=None ):

        """ See IGroupsPlugin.
        """
        grps = []
        DGD = DynamicGroupDefinition.meta_type
        for group in self.objectValues( DGD ):
            if group.active and group( principal, request ):
                grps.append('%s%s' % (self.prefix, group.getId()))
        return grps

    security.declareProtected( ManageGroups, 'enumerateGroups' )
    def enumerateGroups( self
                       , id=None
                       , exact_match=False
                       , sort_by=None
                       , max_results=None
                       , **kw
                       ):
        """ See IGroupEnumerationPlugin.
        """
        group_info = []
        group_ids = []
        plugin_id = self.getId()
        view_name = createViewName('enumerateGroups', id)

        # Look in the cache first...
        keywords = copy.deepcopy(kw)
        keywords.update( { 'id' : id
                         , 'exact_match' : exact_match
                         , 'sort_by' : sort_by
                         , 'max_results' : max_results
                         }
                       )
        cached_info = self.ZCacheable_get( view_name=view_name
                                         , keywords=keywords
                                         , default=None
                                         )

        if cached_info is not None:
            return tuple(cached_info)

        if isinstance( id, str ):
            id = [ id ]

        if exact_match and id:
            group_ids.extend( id )

        if group_ids:
            group_filter = None

        else:   # Searching
            group_ids = self.listGroupIds()
            group_filter = _DynamicGroupFilter( id, **kw )

        known = self.listGroupIds()
        for group_id in group_ids:
            g_info = self.getGroupInfo(group_id, raise_keyerror=False)
            if g_info is not None:

                url = '/%s/%s/manage_propertiesForm' % (
                            self.absolute_url(1), group_id)
                info = {}
                info.update( self.getGroupInfo( group_id ) )

                info[ 'pluginid' ] = plugin_id
                info[ 'properties_url' ] = url
                info[ 'members_url' ] = url

                info[ 'id' ] = '%s%s' % (self.prefix, info['id'])

                if not group_filter or group_filter( info ):
                    if info[ 'active' ]:
                        group_info.append( info )

        # Put the computed value into the cache
        self.ZCacheable_set(group_info, view_name=view_name, keywords=keywords)

        return tuple( group_info )

    #
    #   Housekeeping
    #
    security.declareProtected( ManageGroups, 'listGroupIds' )
    def listGroupIds( self ):

        """ Return a list of IDs for the dynamic groups we manage.
        """
        return self.objectIds( DynamicGroupDefinition.meta_type )

    security.declareProtected( ManageGroups, 'getGroupInfo' )
    def getGroupInfo( self, group_id, raise_keyerror=True ):

        """ Return a mappings describing one dynamic group we manage.

        o If 'raise_keyerror' is True, raise KeyError if we don't have an
          existing group definition for 'group_ id'.  Otherwise, return
          None.

        o Keys include:

          'id' -- the group's ID

          'predicate' -- the TALES expression defining group membership

          'active' -- boolean flag:  is the group currently active?
        """
        try:
            original = self._getOb( group_id )
        except AttributeError:
            try:
                original = self._getOb( group_id[len(self.prefix):] )
            except AttributeError:
                if raise_keyerror:
                    raise KeyError, group_id
                else:
                    return None

        if not isinstance( original, DynamicGroupDefinition ):
            if raise_keyerror:
                raise KeyError, group_id
            else:
                return None

        info = {}

        for k, v in original.propertyItems():
            info[ k ] = v

        return info

    security.declareProtected( ManageGroups, 'listGroupInfo' )
    def listGroupInfo( self ):

        """ Return a list of mappings describing the dynamic groups we manage.

        o Keys include:

          'id' -- the group's ID

          'predicate' -- the TALES expression defining group membership

          'active' -- boolean flag:  is the group currently active?
        """
        return [ self.getGroupInfo( x ) for x in self.listGroupIds() ]

    security.declarePrivate( 'addGroup' )
    def addGroup( self
                , group_id
                , predicate
                , title=''
                , description=''
                , active=True
                ):

        """ Add a group definition.

        o Raise KeyError if we have an existing group definition
          for 'group_id'.
        """
        if group_id in self.listGroupIds():
            raise KeyError, 'Duplicate group ID: %s' % group_id

        info = DynamicGroupDefinition( group_id
                                     , predicate
                                     , title
                                     , description
                                     , active
                                     )

        self._setObject( group_id, info )

        # This method changes the enumerateGroups return value
        view_name = createViewName('enumerateGroups')
        self.ZCacheable_invalidate(view_name=view_name)

    security.declarePrivate( 'updateGroup' )
    def updateGroup( self
                   , group_id
                   , predicate
                   , title=None
                   , description=None
                   , active=None
                   ):

        """ Update a group definition.

        o Raise KeyError if we don't have an existing group definition
          for 'group_id'.

        o Don't update 'title', 'description', or 'active' unless supplied.
        """
        if group_id not in self.listGroupIds():
            raise KeyError, 'Invalid group ID: %s' % group_id

        group = self._getOb( group_id )

        group._setPredicate( predicate )

        if title is not None:
            group.title = title

        if description is not None:
            group.description = description

        if active is not None:
            group.active = active

        # This method changes the enumerateGroups return value
        view_name = createViewName('enumerateGroups')
        self.ZCacheable_invalidate(view_name=view_name)
        view_name = createViewName('enumerateGroups', group_id)
        self.ZCacheable_invalidate(view_name=view_name)

    security.declarePrivate( 'removeGroup' )
    def removeGroup( self, group_id ):

        """ Remove a group definition.

        o Raise KeyError if we don't have an existing group definition
          for 'group_id'.
        """
        if group_id not in self.listGroupIds():
            raise KeyError, 'Invalid group ID: %s' % group_id

        self._delObject( group_id )

        # This method changes the enumerateGroups return value
        view_name = createViewName('enumerateGroups')
        self.ZCacheable_invalidate(view_name=view_name)
        view_name = createViewName('enumerateGroups', group_id)
        self.ZCacheable_invalidate(view_name=view_name)

    #
    #   ZMI
    #
    manage_options = ( ( { 'label' : 'Groups'
                         , 'action' : 'manage_groups'
                         }
                       ,
                       )
                     + Folder.manage_options[:1]
                     + BasePlugin.manage_options[:1]
                     + Folder.manage_options[1:]
                     + Cacheable.manage_options
                     )

    manage_groups = PageTemplateFile( 'www/dgpGroups'
                                    , globals()
                                    , __name__='manage_groups'
                                    )

    security.declareProtected( ManageGroups, 'manage_addGroup' )
    @csrf_only
    @postonly
    def manage_addGroup( self
                       , group_id
                       , title
                       , description
                       , predicate
                       , active=True
                       , RESPONSE=None
                       , REQUEST=None
                       ):
        """ Add a group via the ZMI.
        """
        self.addGroup( group_id
                     , predicate
                     , title
                     , description
                     , active
                     )

        message = 'Group+%s+added' % group_id

        if RESPONSE is not None:
            RESPONSE.redirect( '%s/manage_groups?manage_tabs_message=%s'
                            % ( self.absolute_url(), message )
                            )

    security.declareProtected( ManageGroups, 'manage_updateGroup' )
    @csrf_only
    @postonly
    def manage_updateGroup( self
                          , group_id
                          , predicate
                          , title=None
                          , description=None
                          , active=True
                          , RESPONSE=None
                          , REQUEST=None
                          ):
        """ Update a group via the ZMI.
        """
        self.updateGroup( group_id
                        , predicate
                        , title
                        , description
                        , active
                        )

        message = 'Group+%s+updated' % group_id

        if RESPONSE is not None:
            RESPONSE.redirect( ( '%s/manage_groups?group_id=%s&'
                               + 'manage_tabs_message=%s'
                               ) % ( self.absolute_url(), group_id, message )
                             )

    security.declareProtected( ManageGroups, 'manage_removeGroups' )
    @csrf_only
    @postonly
    def manage_removeGroups( self
                           , group_ids
                           , RESPONSE=None
                           , REQUEST=None
                           ):
        """ Remove one or more groups via the ZMI.
        """
        group_ids = filter( None, group_ids )

        if not group_ids:
            message = 'no+groups+selected'

        else:

            for group_id in group_ids:
                self.removeGroup( group_id )

            message = 'Groups+removed'

        if RESPONSE is not None:
            RESPONSE.redirect( '%s/manage_groups?manage_tabs_message=%s'
                             % ( self.absolute_url(), message )
                             )

classImplements( DynamicGroupsPlugin
               , IDynamicGroupsPlugin
               , IGroupsPlugin
               , IGroupEnumerationPlugin
               )

InitializeClass( DynamicGroupsPlugin )

class _DynamicGroupFilter:

    def __init__( self
                , id=None
                , **kw
                ):

        self._filter_ids = id

    def __call__( self, group_info ):

        if self._filter_ids:

            key = 'id'

        else:
            return 1 # TODO:  try using 'kw'

        value = group_info.get( key )

        if not value:
            return 0

        for id in self._filter_ids:
            if value.find( id ) >= 0:
                return 1

        return 0
