#####################################################################
#
# LDAPUserFolder	An LDAP-based user source for Zope
#
# This software is governed by a license. See
# LICENSE.txt for the terms of this license.
#
#####################################################################
__version__='$Revision: 1.142 $'[11:-2]

# General python imports
import time, os, urllib

# Zope imports
from Globals import DTMLFile, package_home, InitializeClass
from Acquisition import aq_base, aq_inner, aq_parent
from AccessControl import ClassSecurityInfo
from AccessControl.User import BasicUserFolder, domainSpecMatch
from AccessControl.SecurityManagement import getSecurityManager
from AccessControl.Permissions import view_management_screens, \
                                      manage_users, \
                                      view
from OFS.SimpleItem import SimpleItem
from BTrees.OOBTree import OOBTree

# LDAPUserFolder package imports
from LDAPUser import LDAPUser
from LDAPDelegate import LDAPDelegate, explode_dn
from LDAPDelegate import ADD, DELETE, REPLACE, BASE
from SimpleLog import SimpleLog
from SimpleCache import SimpleCache, SharedObject
from SharedResource import getResource
from utils import _createLDAPPassword, to_utf8, crypt
from utils import ldap_scopes, GROUP_MEMBER_MAP, filter_format

_marker = []
_dtmldir = os.path.join(package_home(globals()), 'dtml')
addLDAPUserFolderForm = DTMLFile('addLDAPUserFolder', _dtmldir)
EDIT_PERMISSION = 'Change user folder'


class LDAPUserFolder(BasicUserFolder):
    """ 
        LDAPUserFolder

        The LDAPUserFolder is a user database.  It contains management 
        hooks so that it can be added to a Zope folder as an 'acl_users'
        database.  Its important public method is validate() which
        returns a Zope user object of type LDAPUser
    """
    security = ClassSecurityInfo()

    meta_type = 'LDAPUserFolder'
    id = 'acl_users'
    isAUserFolder = 1


    #################################################################
    #
    # Setting up all ZMI management screens and default login pages
    #
    #################################################################

    manage_options=(
        (
        {'label' : 'Configure',	'action' : 'manage_main', 
         'help'  : ('LDAPUserFolder','Configure.stx')},
        {'label' : 'LDAP Schema', 'action' : 'manage_ldapschema',
         'help'  : ('LDAPUserFolder', 'Schema.stx')},
        {'label' : 'Caches', 'action' : 'manage_cache',
         'help'  : ('LDAPUserFolder', 'Caches.stx')},
        {'label' : 'Users', 'action' : 'manage_userrecords',
         'help'  : ('LDAPUserFolder', 'Users.stx')},
        {'label' : 'Groups', 'action' : 'manage_grouprecords',
         'help' : ('LDAPUserFolder', 'Groups.stx')},
        {'label' : 'Log', 'action' : 'manage_log',
         'help'  : ('LDAPUserFolder', 'Log.stx')},
        )
        + SimpleItem.manage_options
        ) 

    security.declareProtected(view_management_screens, 'manage')
    security.declareProtected(view_management_screens, 'manage_main')
    manage = manage_main = DTMLFile('dtml/properties', globals())
    manage_main._setName('manage_main')
    
    security.declareProtected(view_management_screens, 'manage_ldapschema')
    manage_ldapschema = DTMLFile('dtml/ldapschema', globals())
    
    security.declareProtected(view_management_screens, 'manage_log')
    manage_log = DTMLFile('dtml/log', globals())

    security.declareProtected(view_management_screens, 'manage_cache')
    manage_cache = DTMLFile('dtml/cache', globals())

    security.declareProtected(view_management_screens, 'manage_userrecords')
    manage_userrecords = DTMLFile('dtml/users', globals())
    
    security.declareProtected(view_management_screens, 'manage_grouprecords')
    manage_grouprecords = DTMLFile('dtml/groups', globals())
    

    #################################################################
    #
    # Initialization code
    #
    #################################################################


    def __setstate__(self, v):
        """
            __setstate__ is called whenever the instance is loaded
            from the ZODB, like when Zope is restarted.
        """
        # Call inherited __setstate__ methods if they exist
        LDAPUserFolder.inheritedAttribute('__setstate__')(self, v)

        # "Upgrade" the log attribute if needed
        if hasattr(self, '_log') and isinstance(self._log, SimpleLog):
            del self._log

        # Create a hash value which is used as a unique identifier
        # for the global resource retrieval. Every instance needs a
        # unique hash because *all instances share the cache!*
        if not hasattr(self, '_hash'):
            self._hash = '%s%s' % (self.meta_type, str(time.time()))

        # Reset user caches
        anon_timeout = self.getCacheTimeout('anonymous')
        self._cache('anonymous').setTimeout(anon_timeout)

        auth_timeout = self.getCacheTimeout('authenticated')
        self._cache('authenticated').setTimeout(auth_timeout)

        # Make sure we always have the verbose attribute, otherwise logs break
        if not hasattr(self, 'verbose'):
            self.verbose = 2

        if self.verbose > 2:
            self._log().log(3, 'LDAPUserFolder reinitialized by __setstate__')

        # For older LDAPUserFolders which don't distinguish between uid
        # and login id attrs, provide a _uid_attr
        if not hasattr(self, '_uid_attr'):
            self._uid_attr = self._login_attr


    def __init__( self, title, LDAP_server, login_attr , uid_attr, users_base
                , users_scope, roles , groups_base, groups_scope, binduid
                , bindpwd, binduid_usage, rdn_attr, local_groups=0
                , encryption='SHA', use_ssl=0, read_only=0, REQUEST=None
                ):
        """ Create a new LDAPUserFolder instance """
        self._hash = '%s%s' % (self.meta_type, str(time.time()))
        self.verbose = 2    # _log needs it
        self._delegate = LDAPDelegate()
        self._ldapschema = { 'cn' : { 'ldap_name' : 'cn'
                                    , 'friendly_name' : 'Canonical Name'
                                    , 'multivalued' : ''
                                    , 'public_name' : ''
                                    }
                           , 'sn' : { 'ldap_name' : 'sn'
                                    , 'friendly_name' : 'Last Name'
                                    , 'multivalued' : ''
                                    , 'public_name' : ''
                                    }
                           }

        # Local DN to role tree for storing roles
        self._groups_store = OOBTree()
        # List of additionally known roles
        self._additional_groups = []
        # Place to store mappings from LDAP group to Zope role
        self._groups_mappings = {}

        # Caching-related
        self._anonymous_timeout = 600
        self._authenticated_timeout = 600

        if LDAP_server.find(':') != -1:
            self.LDAP_server = LDAP_server.split(':')[0].strip()
            self.LDAP_port = int(LDAP_server.split(':')[1])
        else:
            if use_ssl:
                self.LDAP_port = 636
            else:
                self.LDAP_port = 389

            self.LDAP_server = LDAP_server.strip()

        if not not use_ssl:
            self._conn_proto = 'ldaps'
        else:
            self._conn_proto = 'ldap'

        self._delegate.addServer( self.LDAP_server
                                , self.LDAP_port
                                , use_ssl
                                )

        self.manage_edit( title=title
                        , login_attr=login_attr
                        , uid_attr=uid_attr
                        , users_base=users_base
                        , users_scope=users_scope
                        , roles=roles
                        , groups_base=groups_base
                        , groups_scope=groups_scope
                        , binduid=binduid
                        , bindpwd=bindpwd
                        , binduid_usage=binduid_usage
                        , rdn_attr=rdn_attr
                        , obj_classes='top,person'
                        , local_groups=0
                        , encryption=encryption
                        , read_only=read_only
                        )


    security.declarePrivate('_clearCaches')
    def _clearCaches(self):
        """ Clear all logs and caches for user-related information """
        self._cache('anonymous').clear()
        self._cache('authenticated').clear()
        self._misc_cache().clear()

    security.declarePrivate('_lookupuserbyattr')
    def _lookupuserbyattr(self, name, value, pwd=None):
        """
            returns a record's DN and the groups a uid belongs to 
            as well as a dictionary containing user attributes
        """
        if name == 'dn':
            if value.find(',') == -1:
                # micro-optimization: this is not a valid dn because it
                # doesn't contain any commas; don't bother trying to look it
                # up
                msg = '_lookupuserbyattr: not a valid dn "%s"' % value
                self._log().log(9, msg)
                return None, None, None

            users_base = to_utf8(value)
            search_str = '(objectClass=*)'

        else:
            users_base = self.users_base
            ob_flt = [filter_format('(%s=%s)', (name, value))]
            ob_flt.extend( [filter_format('(%s=%s)', ('objectClass', o))
                            for o in self._user_objclasses] )
            search_str = '(&%s)' % ''.join(ob_flt)

        # Step 1: Bind either as the Manager or anonymously to look
        #         up the user from the login given
        if self._binduid_usage > 0:
            bind_dn = self._binduid
            bind_pwd = self._bindpwd
        else:
            bind_dn = bind_pwd = ''

        if self.verbose > 8:
            # If you want to log the password as well, which can introduce
            # security problems, uncomment the next lines and comment out
            # the line after that, then restart Zope.
            #msg = '_lookupuserbyattr: Binding as "%s:%s"' % ( bind_dn
            #                                                , bind_pwd
            #                                                )
            msg = '_lookupuserbyattr: Binding as "%s"' % bind_dn
            self._log().log(9, msg)
            msg = '_lookupuserbyattr: Using filter "%s"' % search_str
            self._log().log(9, msg)

        known_attrs = self.getSchemaConfig().keys()

        res = self._delegate.search( base=users_base
                                   , scope=self.users_scope
                                   , filter=search_str
                                   , attrs=known_attrs
                                   , bind_dn=bind_dn
                                   , bind_pwd=bind_pwd
                                   )

        if res['size'] == 0 or res['exception']:
            msg = '_lookupuserbyattr: No user "%s=%s" (%s)' % (
                name, value, res['exception'] or 'n/a'
                )
            self.verbose > 3 and self._log().log(4, msg)
            return None, None, None

        user_attrs = res['results'][0]
        dn = user_attrs.get('dn')
        utf8_dn = to_utf8(dn)

        if pwd is not None:
            # Step 2: Re-bind using the password passed in and the DN we
            #         looked up in Step 1. This will catch bad passwords.
            if self._binduid_usage != 1:
                user_dn = dn
                user_pwd = pwd
            else:
                user_dn = self._binduid
                user_pwd = self._bindpwd

                # Even though I am going to use the Manager DN and password
                # for the "final" lookup I *must* ensure that the password
                # is not a bad password. Since LDAP passwords
                # are one-way encoded I must ask the LDAP server to verify
                # the password, I cannot do it myself.
                try:
                    self._delegate.connect(bind_dn=utf8_dn, bind_pwd=pwd)
                except:
                    # Something went wrong, most likely bad credentials
                    msg = '_lookupuserbyattr: Binding as "%s" fails' % dn
                    self.verbose > 3 and self._log().log(4, msg)
                    return None, None, None

            if self.verbose > 8:
                msg = '_lookupuserbyattr: Re-binding as "%s"' % user_dn
                self._log().log(9, msg)

            auth_res = self._delegate.search( base=utf8_dn
                                            , scope=BASE
                                            , filter='(objectClass=*)'
                                            , attrs=known_attrs
                                            , bind_dn=user_dn
                                            , bind_pwd=user_pwd
                                            )

            if auth_res['size'] == 0 or auth_res['exception']:
                msg = '_lookupuserbyattr: "%s" lookup fails bound as "%s"' % (
                    dn, user_dn
                    )
                self.verbose > 3 and self._log().log(4, msg)
                return None, None, None
            
            user_attrs = auth_res['results'][0]

        else:
            user_pwd = pwd

        self.verbose > 4 and self._log().log(5,
             '_lookupuserbyattr: user_attrs = %s' % str(user_attrs))

        groups = list(self.getGroups(dn=dn, attr='cn', pwd=user_pwd))
        roles = self._mapRoles(groups)
        roles.extend(self._roles)

        return roles, dn, user_attrs


    security.declareProtected(manage_users, 'manage_reinit')
    def manage_reinit(self, REQUEST=None):
        """ re-initialize and clear out users and log """
        self._clearCaches()
        self.verbose > 2 and self._log().log(3, 'Cleared caches on Caches tab')

        if REQUEST:
            msg = 'User caches cleared'
            return self.manage_cache(manage_tabs_message=msg)


    security.declarePrivate('_setProperty')
    def _setProperty(self, prop_name, prop_value):
        """ Set a property on the LDAP User Folder object """
        if not hasattr(self, prop_name):
            msg = 'No property "%s" on the LDAP User Folder' % prop_name
            raise AttributeError, msg

        setattr(self, prop_name, prop_value)


    security.declareProtected(EDIT_PERMISSION, 'manage_changeProperty')
    def manage_changeProperty( self
                             , prop_name
                             , prop_value
                             , client_form='manage_main'
                             , REQUEST=None
                             ):
        """ The public front end for changing single properties """
        try:
            self._setProperty(prop_name, prop_value)
            self._clearCaches()
            msg = 'Attribute "%s" changed.' % prop_name
        except AttributeError, e:
            msg = e.args[0]

        if REQUEST is not None:
            form = getattr(self, client_form)
            return form(manage_tabs_message=msg)


    security.declareProtected(EDIT_PERMISSION, 'manage_edit')
    def manage_edit( self, title, login_attr, uid_attr, users_base
                   , users_scope, roles,  groups_base, groups_scope
                   , binduid, bindpwd, binduid_usage=1, rdn_attr='cn'
                   , obj_classes='top,person', local_groups=0
                   , encryption='SHA', read_only=0, REQUEST=None
                   ):
        """ Edit the LDAPUserFolder Object """
        if not binduid:
            binduid_usage = 0

        self.title = title
        self.users_base = users_base
        self.users_scope = users_scope
        self.groups_base = groups_base or users_base
        self.groups_scope = groups_scope
        self.read_only = not not read_only

        self._delegate.edit( login_attr=login_attr,
                             users_base=users_base,
                             rdn_attr=rdn_attr,
                             objectclasses=obj_classes,
                             bind_dn=binduid,
                             bind_pwd=bindpwd,
                             binduid_usage=binduid_usage,
                             read_only=read_only,
                           )

        if isinstance(roles, str) or isinstance (roles, unicode):
            roles = [x.strip() for x in roles.split(',')]
        self._roles = roles

        self._binduid = binduid
        self._bindpwd = bindpwd
        self._binduid_usage = int(binduid_usage)

        self._local_groups = not not local_groups

        if encryption == 'crypt' and crypt is None:
            encryption = 'SHA'

        self._pwd_encryption = encryption

        if isinstance(obj_classes, str) or isinstance(obj_classes, unicode):
            obj_classes = [x.strip() for x in obj_classes.split(',')]
        self._user_objclasses = obj_classes

        my_attrs = self.getSchemaConfig().keys()

        if rdn_attr not in my_attrs:
            self.manage_addLDAPSchemaItem( ldap_name=rdn_attr
                                         , friendly_name=rdn_attr
                                         )
        self._rdnattr = rdn_attr

        if login_attr != 'dn' and login_attr not in my_attrs:
            self.manage_addLDAPSchemaItem( ldap_name=login_attr
                                         , friendly_name=login_attr
                                         )
        self._login_attr = login_attr

        if uid_attr != 'dn' and uid_attr not in my_attrs:
            self.manage_addLDAPSchemaItem( ldap_name=uid_attr
                                         , friendly_name=uid_attr
                                         )
        self._uid_attr = uid_attr
        
        self._clearCaches()
        self.verbose > 1 and self._log().log(2, 'Properties changed')
        msg = 'Properties changed'

        try:
            connection = self._delegate.connect()
        except RuntimeError:
            connection = None

        if connection is None:
            msg = 'Cannot+connect+to+LDAP+server'

        if REQUEST:
            return self.manage_main(manage_tabs_message=msg)


    security.declareProtected(manage_users, 'manage_addServer')
    def manage_addServer(self, host, port='389', use_ssl=0, REQUEST=None):
        """ Add a new server to the list of servers in use """
        self._delegate.addServer(host, port, use_ssl)
        msg = 'Server at %s:%s added' % (host, port)

        if REQUEST:
            return self.manage_main(manage_tabs_message=msg)


    security.declareProtected(manage_users, 'getServers')
    def getServers(self):
        """ Proxy method used for the ZMI """
        return tuple(self._delegate.getServers())


    security.declareProtected(manage_users, 'manage_deleteServers')
    def manage_deleteServers(self, position_list=[], REQUEST=None):
        """ Delete servers from the list of servers in use """
        if len(position_list) == 0:
            msg = 'No servers selected'
        else:
            self._delegate.deleteServers(position_list)
            msg = 'Servers deleted'

        if REQUEST:
            return self.manage_main(manage_tabs_message=msg)


    security.declareProtected(manage_users, 'getMappedUserAttrs')
    def getMappedUserAttrs(self):
        """ Return the mapped user attributes """
        schema = self.getSchemaDict()
        pn = 'public_name'
        ln = 'ldap_name'

        return tuple([(x[ln], x[pn]) for x in schema if x.get(pn, '')])


    security.declareProtected(manage_users, 'getMultivaluedUserAttrs')
    def getMultivaluedUserAttrs(self):
        """ Return sequence of user attributes that are multi-valued"""
        schema = self.getSchemaDict()
        mv = [x['ldap_name'] for x in schema if x.get('multivalued', '')]

        return tuple(mv)


    security.declareProtected(manage_users, 'getUsers')
    def getUsers(self, authenticated=1):
        """Return a list of *cached* user objects"""
        if authenticated:
            return self._cache('authenticated').getCache()
        else:
            return self._cache('anonymous').getCache()


    security.declareProtected(manage_users, 'getAttributeOfAllObjects')
    def getAttributesOfAllObjects(self, base_dn, scope, filter_str, attrnames):
        """ Return a dictionary keyed on attribute name where each value
        in the dict is a sequence of attribute values specified by 'attrnames'
        for all objects in the given base dn and scope filtered via filter_str.
        The attributes searched are assumed to be single-valued.  If an
        attribute is multivalued, only the first value of the attribute
        will be included in the returned structure.
        """
        result_dict = {}

        res = self._delegate.search( base=base_dn
                                   , scope=scope
                                   , attrs=attrnames
                                   , filter=filter_str
                                   )

        if res['size'] == 0 or res['exception']:
            msg = ('getAttributesOfAllObjects: Cannot find any users (%s)'
                   % res['exception'])
            self._log().log(2, msg)

            return []

        result_dicts = res['results']

        for attrname in attrnames:
            result_dict[attrname] = s = []
            for i in range(res['size']):

                if attrname == 'dn':
                    s.append(result_dicts[i].get(attrname))

                else:
                    result = result_dicts[i].get(attrname, [])
                    if len(result) == 0:
                        result = ''
                    elif len(result) > 0:
                        result = result[0]
                    s.append(result)

        return result_dict


    security.declareProtected(manage_users, 'getUserIds')
    def getUserIds(self):
        """ Return a tuple containing all user IDs """
        expires = self._misc_cache().get('useridlistexpires')
        if expires and expires > time.time():
            return self._misc_cache().get('useridlist')

        user_filter = self._getUserFilterString()
        
        useridlist = self.getAttributesOfAllObjects(
            self.users_base, ldap_scopes[self.users_scope],
            user_filter, (self._uid_attr,))[self._uid_attr]

        useridlist.sort()

        self._misc_cache().set('useridlist', useridlist[:])
        # Expire after 600 secs
        self._misc_cache().set('useridlistexpires', time.time() + 600)

        return tuple(useridlist)


    security.declareProtected(manage_users, 'getUserNames')
    def getUserNames(self):
        """ Return a tuple containing all logins """
        expires = self._misc_cache().get('loginlistexpires')
        if expires and expires > time.time():
            return self._misc_cache().get('loginlist')

        user_filter = self._getUserFilterString()

        loginlist = self.getAttributesOfAllObjects(
            self.users_base, ldap_scopes[self.users_scope],
            user_filter, (self._login_attr,))[self._login_attr]

        loginlist.sort()

        self._misc_cache().set('loginlist', loginlist[:])
        # Expire after 600 secs
        self._misc_cache().set('loginlistexpires', time.time() + 600)

        return tuple(loginlist)


    security.declareProtected(manage_users, 'getUserIdsAndNames')
    def getUserIdsAndNames(self):
        """ Return a tuple of (user ID, login) tuples """
        expires = self._misc_cache().get('useridnamelistexpires')
        if expires and expires > time.time():
            return self._misc_cache().get('useridnamelist')

        user_filter = self._getUserFilterString()

        d = self.getAttributesOfAllObjects(
            self.users_base, ldap_scopes[self.users_scope],
            user_filter, (self._uid_attr, self._login_attr))

        login_id_list = zip(d[self._uid_attr], d[self._login_attr])

        login_id_list.sort()
        self._misc_cache().set('useridnamelist', login_id_list)
        # Expire after 600 secs
        self._misc_cache().set('useridnamelistexpires', time.time() + 600)

        return tuple(login_id_list)


    security.declarePrivate('_getUserFilterString')
    def _getUserFilterString(self):
        """ Return filter string suitable for querying on user objects """
        user_filter = [filter_format('(%s=%s)', ('objectClass', o))
                       for o in filter(None, self._user_objclasses)]
        if user_filter:
            user_filter = '(&%s)' % ''.join(user_filter)
        else:
            user_filter = '(objectClass=*)'

        return user_filter


    security.declareProtected(manage_users, 'getUserByAttr')
    def getUserByAttr(self, name, value, pwd=None, cache=0):
        """ 
            Get a user based on a name/value pair representing an
            LDAP attribute provided to the user.  If cache is True,
            try to cache the result using 'value' as the key 
        """
        if not value:
            return None

        cache_type = pwd and 'authenticated' or 'anonymous'
        if cache:
            cached_user = self._cache(cache_type).get(value, pwd)

            if cached_user:
                if self.verbose > 6:
                    msg = 'getUserByAttr: "%s" cached in %s cache' % (
                        value, cache_type
                        )
                    self._log().log(7, msg)
                return cached_user

        user_roles, user_dn, user_attrs = self._lookupuserbyattr(
            name=name, value=value, pwd=pwd
            )

        if user_dn is None:
            msg = 'getUserByAttr: "%s=%s" not found' % (name, value)
            self.verbose > 3 and self._log().log(4, msg)
            return None

        if user_attrs is None:
            msg = 'getUserByAttr: "%s=%s" has no properties, bailing' % (
                name, value
                )
            self.verbose > 3 and self._log().log(4, msg)
            return None

        if user_roles is None or user_roles == self._roles:
            msg = 'getUserByAttr: "%s=%s" only has roles %s' % (
                name, value, str(user_roles)
                )
            self.verbose > 8 and self._log().log(9, msg)

        login_name = user_attrs.get(self._login_attr)
        uid = user_attrs.get(self._uid_attr)

        if self._login_attr != 'dn' and len(login_name) > 0:
            if name == self._login_attr:
                logins = [x for x in login_name if value.lower() == x.lower()]
                login_name = logins[0]
            else:
                login_name = login_name[0]
        elif len(login_name) == 0:
            msg = 'getUserByAttr: "%s" has no "%s" (Login) value!' % (
                    user_dn, self._login_attr
                    )
            self.verbose > 3 and self._log().log(4, msg)
            return None

        if self._uid_attr != 'dn' and len(uid) > 0:
            uid = uid[0]
        elif len(uid) == 0:
            msg = 'getUserByAttr: "%s" has no "%s" (UID Attribute) value!' % (
                    user_dn, self._uid_attr
                    )
            self.verbose > 3 and self._log().log(4, msg)
            return None

        user_obj = LDAPUser( uid
                           , login_name
                           , pwd or 'undef'
                           , user_roles or []
                           , []
                           , user_dn
                           , user_attrs
                           , self.getMappedUserAttrs()
                           , self.getMultivaluedUserAttrs()
                           )

        if cache:
            self._cache(cache_type).set(value, user_obj)

        return user_obj


    security.declareProtected(manage_users, 'getUser')
    def getUser(self, name, pwd=None):
        """Return a user object specified by its username or None """
        # we want to cache based on login attr, because it's the
        # most-frequently-used codepath
        user = self.getUserByAttr(self._login_attr, name, pwd, cache=1)

        return user


    security.declareProtected(manage_users, 'getUserById')
    def getUserById(self, id, default=_marker):
        """ Return a user object specified by its user id or None """
        user = self.getUserByAttr(self._uid_attr, id)
        if user is None and default is not _marker:
            return default

        return user


    def getUserByDN(self, user_dn):
        """ Make a user object from a DN """
        uid_attr = self._uid_attr

        res = self._delegate.search( base=user_dn
                                   , scope=BASE
                                   , attrs=[uid_attr]
                                   )

        if res['exception'] or res['size'] == 0:
            return None

        if uid_attr != 'dn':
            user_id = res['results'][0].get(uid_attr)[0]
        else:
            user_id = to_utf8(res['results'][0].get(uid_attr))

        user = self.getUserByAttr(uid_attr, user_id)

        return user


    def authenticate(self, name, password, request):
        super = self._emergency_user

        if not name:
            return None

        if super and name == super.getUserName():
            user = super
        else:
            user = self.getUser(name, password)

        if user is not None:
            domains = user.getDomains()
            if domains:
                return (domainSpecMatch(domains, request) and user) or None
            
        return user


    #################################################################
    #
    # Stuff formerly in LDAPShared.py
    #
    #################################################################

    security.declareProtected(manage_users, 'getUserDetails')
    def getUserDetails(self, encoded_dn, format=None, attrs=[]):
        """ Return all attributes for a given DN """
        dn = to_utf8(urllib.unquote(encoded_dn))

        res = self._delegate.search( base=dn
                                   , scope=BASE
                                   , attrs=attrs
                                   )

        if res['exception']:
            if format == None:
                result = ((res['exception'], res),)
            elif format == 'dictionary':
                result = { 'cn': '###Error: %s' % res['exception'] }
        elif res['size'] > 0:
            value_dict = res['results'][0]

            if format == None:
                result = value_dict.items()
                result.sort()
            elif format == 'dictionary':
                result = value_dict
        else:
            if format == None:
                result = ()
            elif format == 'dictionary':
                result = {}

        return result


    security.declareProtected(manage_users, 'getGroupDetails')
    def getGroupDetails(self, encoded_cn):
        """ Return all group details """
        result = ()
        cn = urllib.unquote(encoded_cn)

        if not self._local_groups:
            res = self._delegate.search( base=self.groups_base
                                       , scope=self.groups_scope
                                       , filter=filter_format('(cn=%s)', (cn,))
                                       , attrs=['uniqueMember', 'member']
                                       )

            if res['exception']:
                exc = res['exception']
                msg = 'getGroupDetails: No group "%s" (%s)' % (cn, exc)
                self._log().log(3, msg)
                result = (('Exception', exc),)

            elif res['size'] > 0:
                result = res['results'][0].items()
                result.sort()

            else:
                msg = 'getGroupDetails: No group "%s"' % cn
                self._log().log(3, msg)

        else:
            g_dn = ''
            all_groups = self.getGroups()
            for group_cn, group_dn in all_groups:
                if group_cn == cn:
                    g_dn = group_dn
                    break

            if g_dn:
                users = []

                for user_dn, role_dns in self._groups_store.items():
                    if g_dn in role_dns:
                        users.append(user_dn)

                result = [('', users)]

        return result


    security.declareProtected(manage_users, 'getGroupedUsers')
    def getGroupedUsers(self, groups=None):
        """ Return all those users that are in a group """
        all_dns = {}
        users = []
        member_attrs = GROUP_MEMBER_MAP.values()

        if groups is None:
            groups = self.getGroups()

        for group_id, group_dn in groups:
            group_details = self.getGroupDetails(group_id)
            for key, vals in group_details:
                if key in member_attrs or key == '':
                    # If the key is an empty string then the groups are
                    # stored inside the user folder itself.
                    for dn in vals:
                        all_dns[dn] = 1

        for dn in all_dns.keys():
            try:
                user = self.getUserByDN(dn)
            except: 
                user = None

            if user is not None:
                users.append(user.__of__(self))

        return tuple(users)


    security.declareProtected(manage_users, 'getLocalUsers')
    def getLocalUsers(self):
        """ Return all those users who are in locally stored groups """
        local_users = []

        for user_dn, user_roles in self._groups_store.items():
            local_users.append((user_dn, user_roles))

        return tuple(local_users)


    security.declareProtected(manage_users, 'findUser')
    def findUser(self, search_param, search_term, attrs=[]):
        """ Look up matching user records based on attributes """
        lscope = ldap_scopes[self.users_scope]
        users  = []

        if search_param == 'dn':
            users_base = search_term
            search_str = '(objectClass=*)'
        else:
            users_base = self.users_base
            if search_term:
                search_str = filter_format( '(%s=*%s*)'
                                          , (search_param, search_term)
                                          )
            else:
                search_str = '(%s=*)' % search_param

        res = self._delegate.search( base=users_base
                                   , scope=self.users_scope
                                   , filter=search_str
                                   , attrs=attrs
                                   )

        if res['exception']:
            msg = 'findUser Exception (%s)' % res['exception']
            self.verbose > 1 and self._log().log(2, msg)
            msg = 'findUser searched term "%s", param "%s"' % ( search_term
                                                              , search_param
                                                              )
            self.verbose > 8 and self._log().log(9, msg)
            users = [{ 'dn' : res['exception']
                     , 'cn' : 'n/a'
                     , 'sn' : 'Error'
                     }]

        elif res['size'] > 0:
            res_dicts = res['results']
            for i in range(res['size']):
                dn = res_dicts[i].get('dn')
                rec_dict = {}
                rec_dict['sn'] = rec_dict['cn'] = ''

                for key, val in res_dicts[i].items():
                    rec_dict[key] = val[0]

                rec_dict['dn'] = dn

                users.append(rec_dict)

        return users


    security.declareProtected(manage_users, 'getGroups')
    def getGroups(self, dn='*', attr=None, pwd=''):
        """
            returns a list of possible groups from the ldap tree
            (Used e.g. in showgroups.dtml) or, if a DN is passed
            in, all groups for that particular DN.
        """
        group_list = []
        no_show = ('Anonymous', 'Authenticated', 'Shared')

        if self._local_groups:
            if dn != '*':
                all_groups_list = self._groups_store.get(dn) or []
            else:
                all_groups_dict = {}
                zope_roles = list(self.valid_roles())
                zope_roles.extend(list(self._additional_groups))

                for role_name in zope_roles:
                    if role_name not in no_show:
                        all_groups_dict[role_name] = 1

                all_groups_list = all_groups_dict.keys()

            for group in all_groups_list:
                if attr is None:
                    group_list.append((group, group))
                else:
                    group_list.append(group)

            group_list.sort()

        else:
            gscope = ldap_scopes[self.groups_scope]

            if dn != '*':
                f_template = '(&(objectClass=%s)(%s=%s))'
                group_filter = '(|'

                for g_name, m_name in GROUP_MEMBER_MAP.items():
                    fltr = filter_format(f_template, (g_name, m_name, dn))
                    group_filter += fltr

                group_filter += ')'
                    
            else:
                group_filter = '(|'

                for g_name in GROUP_MEMBER_MAP.keys():
                    fltr = filter_format('(objectClass=%s)', (g_name,))
                    group_filter += fltr

                group_filter += ')'

            res = self._delegate.search( base=self.groups_base
                                       , scope=gscope
                                       , filter=group_filter
                                       , attrs=['cn']
                                       , bind_dn=''
                                       , bind_pwd=''
                                       )

            exc = res['exception']
            if exc:
                if attr is None:
                    group_list = (('', exc),)
                else:
                    group_list = (exc,)
            elif res['size'] > 0:
                res_dicts = res['results']
                for i in range(res['size']):
                    dn = res_dicts[i].get('dn')
                    try:
                        cn = res_dicts[i]['cn'][0]
                    except KeyError:    # NDS oddity
                        cn = explode_dn(dn, 1)[0]

                    if attr is None:
                        group_list.append((cn, dn))
                    elif attr == 'cn':
                        group_list.append(cn)
                    elif attr == 'dn':
                        group_list.append(dn)

        return group_list


    security.declareProtected(manage_users, 'getGroupType')
    def getGroupType(self, group_dn):
        """ get the type of group """
        if self._local_groups:
            if group_dn in self._additional_groups:
                group_type = 'Custom Role'
            else:
                group_type = 'Zope Built-in Role'

        else:
            group_type = 'n/a'
            res = self._delegate.search( base=to_utf8(group_dn)
                                       , scope=BASE
                                       , attrs=['objectClass']
                                       )

            if res['exception']:
                msg = 'getGroupType: No group "%s" (%s)' % ( group_dn
                                                           , res['exception']
                                                           )
                self.verbose > 1 and self._log().log(2, msg)

            else:
                groups = GROUP_MEMBER_MAP.keys()
                l_groups = [x.lower() for x in groups]
                g_attrs = res['results'][0]
                group_obclasses = g_attrs.get('objectClass', [])
                group_obclasses.extend(g_attrs.get('objectclass', []))
                g_types = [x for x in group_obclasses if x.lower() in l_groups]

                if len(g_types) > 0:
                    group_type = g_types[0]

        return group_type


    security.declareProtected(manage_users, 'getGroupMappings')
    def getGroupMappings(self):
        """ Return the dictionary that maps LDAP groups map to Zope roles """
        mappings = getattr(self, '_groups_mappings', {})

        return mappings.items()


    security.declareProtected(manage_users, 'manage_addGroupMapping')
    def manage_addGroupMapping(self, group_name, role_name, REQUEST=None):
        """ Map a LDAP group to a Zope role """
        mappings = getattr(self, '_groups_mappings', {})
        mappings[group_name] = role_name
        self._groups_mappings = mappings
        self._clearCaches()

        if REQUEST:
            msg = 'Added LDAP group to Zope role mapping: %s -> %s' % (
                    group_name, role_name)
            return self.manage_grouprecords(manage_tabs_message=msg)


    security.declareProtected(manage_users, 'manage_deleteGroupMappings')
    def manage_deleteGroupMappings(self, group_names, REQUEST=None):
        """ Delete mappings from LDAP group to Zope role """
        mappings = getattr(self, '_groups_mappings', {})

        for group_name in group_names:
            if mappings.has_key(group_name):
                del mappings[group_name]

        self._groups_mappings = mappings
        self._clearCaches()

        if REQUEST:
            msg = 'Deleted LDAP group to Zope role mapping for: %s' % (
                    ', '.join(group_names))
            return self.manage_grouprecords(manage_tabs_message=msg)


    security.declarePrivate('_mapRoles')
    def _mapRoles(self, groups):
        """ Perform the mapping of LDAP groups to Zope roles """
        mappings = getattr(self, '_groups_mappings', {})
        roles = []
        
        for group in groups:
            mapped_role = mappings.get(group, None)
            if mapped_role is not None and mapped_role not in roles:
                roles.append(mapped_role)

        return roles


    security.declareProtected(view_management_screens, 'getProperty')
    def getProperty(self, prop_name, default=''):
        """ Get at LDAPUserFolder properties """
        return getattr(self, prop_name, default)


    security.declareProtected(manage_users, 'getLDAPSchema')
    def getLDAPSchema(self):
        """ Retrieve the LDAP schema this product knows about """
        raw_schema = self.getSchemaDict()
        schema = [(x['ldap_name'], x['friendly_name']) for x in raw_schema]
        schema.sort()

        return tuple(schema)


    security.declareProtected(manage_users, 'getSchemaDict')
    def getSchemaDict(self):
        """ Retrieve schema as list of dictionaries """
        all_items = self.getSchemaConfig().values()
        all_items.sort()

        return tuple(all_items)


    security.declareProtected(EDIT_PERMISSION, 'setSchemaConfig')
    def setSchemaConfig(self, schema):
        """ Set the LDAP schema configuration """
        self._ldapschema = schema
        self._clearCaches()


    security.declareProtected(manage_users, 'getSchemaConfig')
    def getSchemaConfig(self):
        """ Retrieve the LDAP schema configuration """
        return self._ldapschema


    security.declareProtected(EDIT_PERMISSION, 'manage_addLDAPSchemaItem')
    def manage_addLDAPSchemaItem( self
                                , ldap_name
                                , friendly_name=''
                                , multivalued=''
                                , public_name=''
                                , REQUEST=None
                                ):
        """ Add a schema item to my list of known schema items """
        schema = self.getSchemaConfig()
        if ldap_name not in schema.keys():
            schema[ldap_name] = { 'ldap_name' : ldap_name
                                , 'friendly_name' : friendly_name
                                , 'public_name' : public_name
                                , 'multivalued' : multivalued
                                }

            self.setSchemaConfig(schema)
            msg = 'LDAP Schema item "%s" added' % ldap_name
        else:
            msg = 'LDAP Schema item "%s" already exists'  % ldap_name
 
        if REQUEST:
            return self.manage_ldapschema(manage_tabs_message=msg)


    security.declareProtected(EDIT_PERMISSION, 'manage_deleteLDAPSchemaItems')
    def manage_deleteLDAPSchemaItems(self, ldap_names=[], REQUEST=None):
        """ Delete schema items from my list of known schema items """
        if len(ldap_names) < 1:
            msg = 'Please select items to delete'
 
        else:
            schema = self.getSchemaConfig()
            removed = []

            for ldap_name in ldap_names:
                if ldap_name in schema.keys():
                    removed.append(ldap_name)
                    del schema[ldap_name]
 
            self.setSchemaConfig(schema)

            rem_str = ', '.join(removed)
            msg = 'LDAP Schema items %s removed.' % rem_str
 
        if REQUEST:
            return self.manage_ldapschema(manage_tabs_message=msg)


    security.declareProtected(manage_users, 'manage_addGroup')
    def manage_addGroup( self
                       , newgroup_name
                       , newgroup_type='groupOfUniqueNames'
                       , REQUEST=None
                       ):
        """ Add a new group in groups_base """
        if self._local_groups and newgroup_name:
            add_groups = self._additional_groups
            
            if newgroup_name not in add_groups:
                add_groups.append(newgroup_name)

            self._additional_groups = add_groups
            msg = 'Added new group %s' % (newgroup_name)

        elif newgroup_name:
            attributes = {}
            attributes['cn'] = [newgroup_name]
            attributes['objectClass'] = ['top', newgroup_type]

            if self._binduid:
                initial_member = self._binduid
            else:
                user = getSecurityManager().getUser()
                try:
                    initial_member = user.getUserDN()
                except:
                    initial_member = ''

            attributes[GROUP_MEMBER_MAP.get(newgroup_type)] = initial_member

            err_msg = self._delegate.insert( base=self.groups_base
                                           , rdn='cn=%s' % newgroup_name
                                           , attrs=attributes
                                           )
            msg = err_msg or 'Added new group %s' % (newgroup_name)

        else:
            msg = 'No group name specified'

        if REQUEST:
            return self.manage_grouprecords(manage_tabs_message=msg)


    security.declareProtected(manage_users, 'manage_addUser')
    def manage_addUser(self, REQUEST=None, kwargs={}):
        """ Add a new user record to LDAP """
        base = self.users_base
        attr_dict = {}
        
        if REQUEST is None:
            source = kwargs
        else:
            source = REQUEST

        rdn_attr = self._rdnattr
        attr_dict[rdn_attr] = source.get(rdn_attr)
        rdn = '%s=%s' % (rdn_attr, source.get(rdn_attr))
        sub_loc = source.get('sub_branch', '')
        if sub_loc:
            base = '%s,%s' % (rdn, base)
        password = source.get('user_pw', '')
        confirm  = source.get('confirm_pw', '')
        
        if password != confirm or password == '': 
            msg = 'The password and confirmation do not match!'

        else:
            encrypted_pwd = _createLDAPPassword( password
                                               , self._pwd_encryption
                                               )
            attr_dict['userPassword'] = encrypted_pwd
            attr_dict['objectClass'] = self._user_objclasses

            for attribute, names in self.getSchemaConfig().items():
                attr_val = source.get(attribute, None)

                if attr_val:
                    attr_dict[attribute] = attr_val
                elif names.get('public_name', None):
                    attr_val = source.get(names['public_name'], None)

                    if attr_val:
                        attr_dict[attribute] = attr_val

            msg = self._delegate.insert( base=base
                                       , rdn=rdn
                                       , attrs=attr_dict
                                       )

        if msg:
            if REQUEST:
                return self.manage_userrecords(manage_tabs_message=msg)
            else:
                return msg
                

        if not msg:
            user_dn = '%s,%s' % (rdn, base)
            try:
                user_roles = source.get('user_roles', [])

                if self._local_groups:
                    self._groups_store[user_dn] = user_roles
                else:
                    if len(user_roles) > 0:
                        group_dns = []

                        for role in user_roles:
                            try:
                                exploded = explode_dn(role)
                                elements = len(exploded)
                            except:
                                elements = 1

                            if elements == 1:  # simple string
                                role = 'cn=%s,%s' % ( str(role)
                                                    , self.groups_base
                                                    )

                            group_dns.append(role)

                            try:
                                self.manage_editUserRoles(user_dn, group_dns)
                            except:
                                raise

                # Clear the caches for the purpose of clearing any user ID
                # list cached by getUserNames
                self._clearCaches()

                msg = 'New user %s added' % user_dn
            except Exception, e:
                msg = str(e)
                user_dn = ''
            
        if REQUEST:
            return self.manage_userrecords( manage_tabs_message=msg
                                          , user_dn='%s,%s' % (rdn, base)
                                          )


    security.declareProtected(manage_users, 'manage_deleteGroups')
    def manage_deleteGroups(self, dns=[], REQUEST=None):
        """ Delete groups from groups_base """
        msg = ''

        if len(dns) < 1:
            msg = 'You did not specify groups to delete!'

        else:
            if self._local_groups:
                add_groups = self._additional_groups
                for dn in dns:
                    if dn in add_groups:
                        del add_groups[add_groups.index(dn)]

                self._additional_groups = add_groups

            else:
                for dn in dns:
                    msg = self._delegate.delete(dn)

                    if msg:
                        break

            msg = msg or 'Deleted group(s):<br> %s' % '<br>'.join(dns)
            self._clearCaches()
 
        if REQUEST:
            return self.manage_grouprecords(manage_tabs_message=msg)


    security.declareProtected(manage_users, 'manage_deleteUsers')
    def manage_deleteUsers(self, dns=[], REQUEST=None):
        """ Delete all users in list dns """
        if len(dns) < 1:
            msg = 'You did not specify users to delete!'

        elif self._delegate.read_only:
            msg = 'Running in read-only mode, deletion is disabled'
        
        else:
            for dn in dns:
                msg = self._delegate.delete(dn)

                if msg:
                    break

                user_groups = self.getGroups(dn=dn, attr='dn')

                if self._local_groups:
                    if dn in self._groups_store.keys():
                        del self._groups_store[dn]

                    continue

                for group in user_groups:
                    group_type = self.getGroupType(group)
                    member_type = GROUP_MEMBER_MAP.get(group_type)

                    msg = self._delegate.modify( dn=group
                                               , mod_type=DELETE
                                               , attrs={member_type : [dn]}
                                               )

                    if msg:
                        break

            msg = 'Deleted user(s):<br> %s' % '<br>'.join(dns)
            self._clearCaches()

        if REQUEST:
            return self.manage_userrecords(manage_tabs_message=msg)


    security.declareProtected(manage_users, 'manage_editUserPassword')
    def manage_editUserPassword(self, dn, new_pw, REQUEST=None):
        """ Change a user password """
        hidden = '<input type="hidden" name="user_dn" value="%s">' % (dn)
        err_msg = msg = ''

        if new_pw == '':
            msg = 'The password cannot be empty!'

        else:
            ldap_pw = _createLDAPPassword(new_pw, self._pwd_encryption)
            err_msg = self._delegate.modify( dn=dn
                                           , attrs={'userPassword':[ldap_pw]}
                                           )
            if not err_msg:
                msg = 'Password changed for "%s"' % dn
                user_obj = self.getUserByDN(dn)
                self._expireUser(user_obj)

        if REQUEST:
            return self.manage_userrecords( manage_tabs_message=err_msg or msg
                                          , user_dn=dn
                                          )


    security.declareProtected(manage_users, 'manage_editUserRoles')
    def manage_editUserRoles(self, user_dn, role_dns=[], REQUEST=None):
        """ Edit the roles (groups) of a user """
        msg = ''
        all_groups = self.getGroups(attr='dn')
        cur_groups = self.getGroups(dn=user_dn, attr='dn')
        group_dns = []
        for group in role_dns:
            if group.find('=') == -1:
                group_dns.append('cn=%s,%s' % (group, self.groups_base))
            else:
                group_dns.append(group)

        if self._local_groups:
            if len(role_dns) == 0:
                del self._groups_store[user_dn]
            else:
                self._groups_store[user_dn] = role_dns

        else:
            for group in all_groups:
                member_attr = GROUP_MEMBER_MAP.get(self.getGroupType(group))

                if group in cur_groups and group not in group_dns:
                    msg = self._delegate.modify( group
                                               , DELETE
                                               , {member_attr : [user_dn]}
                                               )
                elif group in group_dns and group not in cur_groups:
                    msg = self._delegate.modify( group
                                               , ADD
                                               , {member_attr : [user_dn]}
                                               )

        msg = msg or 'Roles changed for %s' % (user_dn)
        user_obj = self.getUserByDN(user_dn)
        if user_obj is not None:
            self._expireUser(user_obj)

        if REQUEST:
            return self.manage_userrecords( manage_tabs_message=msg
                                          , user_dn=user_dn
                                          )


    security.declareProtected(manage_users, 'manage_setUserProperty')
    def manage_setUserProperty(self, user_dn, prop_name, prop_value):
        """ Set a new attribute on the user record """
        if isinstance(prop_value, str) or isinstance(prop_value, unicode):
            prop_value = [x.strip() for x in prop_value.split(';')]

        for i in range(len(prop_value)):
            prop_value[i] = to_utf8(prop_value[i])

        cur_rec = self._delegate.search( base=user_dn
                                       , scope=BASE
                                       )

        if cur_rec['exception'] or cur_rec['size'] == 0:
            exc = cur_rec['exception']
            msg = 'manage_setUserProperty: No user "%s" (%s)' % (user_dn, exc)
            self.verbose > 1 and self._log().log(2, msg)
                                                                
            return

        user_rec = cur_rec['results'][0]
        cur_prop = user_rec.get(prop_name, [''])

        if cur_prop != prop_value:
            if prop_value != ['']:
                mod = REPLACE
            else:
                mod = DELETE

            err_msg = self._delegate.modify( dn=user_dn
                                           , mod_type=mod
                                           , attrs={prop_name:prop_value}
                                           )
            
            if not err_msg:
                user_obj = self.getUserByDN(user_dn)
                self._expireUser(user_obj)


    security.declareProtected(manage_users, 'manage_editUser')
    def manage_editUser(self, user_dn, REQUEST=None, kwargs={}):
        """ Edit a user record """
        msg = ''
        new_attrs = {}
        utf8_dn = to_utf8(user_dn)
        cur_user = self.getUserByDN(utf8_dn)

        if REQUEST is None:
            source = kwargs
        else:
            source = REQUEST

        for attr in self.getSchemaConfig().keys():
            if source.has_key(attr):
                new = source.get(attr, '')
                if isinstance(new, str) or isinstance(new, unicode):
                    new = [x.strip() for x in new.split(';')]

                new_attrs[attr] = new

        if cur_user is None:
            msg = 'No user with DN "%s"' % user_dn

        if new_attrs and not msg:
            msg = self._delegate.modify(user_dn, attrs=new_attrs)
        elif not new_attrs:
            msg = 'No attributes changed'

        if msg:
            if REQUEST:
                return self.manage_userrecords( manage_tabs_message=msg
                                              , user_dn=user_dn
                                              )
            else:
                return msg

        rdn = self._rdnattr
        new_cn = source.get(rdn, '')
        new_dn = ''

        # This is not good, but explode_dn mangles non-ASCII
        # characters so I simply cannot use it.
        old_utf8_rdn = to_utf8('%s=%s' % (rdn, cur_user.getProperty(rdn)))
        new_rdn = '%s=%s' % (rdn, new_cn)
        new_utf8_rdn = to_utf8(new_rdn)

        if new_cn and new_utf8_rdn != old_utf8_rdn:
            old_dn = utf8_dn
            old_dn_exploded = explode_dn(old_dn)
            old_dn_exploded[0] = new_rdn
            new_dn = ','.join(old_dn_exploded)
            old_groups = self.getGroups(dn=user_dn, attr='dn')

            if self._local_groups:
                if self._groups_store.get(user_dn):
                    del self._groups_store[user_dn]

                self._groups_store[new_dn] = old_groups

            else:
                for group in old_groups:
                    group_type = self.getGroupType(group)
                    member_type = GROUP_MEMBER_MAP.get(group_type)

                    msg = self._delegate.modify( group
                                               , DELETE
                                               , {member_type : [user_dn]}
                                               )
                    msg = self._delegate.modify( group
                                               , ADD
                                               , {member_type : [new_dn]}
                                               )

        self._expireUser(cur_user.getProperty(rdn))
        msg = msg or 'User %s changed' % (new_dn or user_dn)

        if REQUEST:
            return self.manage_userrecords( manage_tabs_message=msg
                                          , user_dn=new_dn or user_dn
                                          )

    security.declareProtected(manage_users, '_expireUser')
    def _expireUser(self, user):
        """ Purge user object from caches """
        user = user or ''

        if not isinstance(user, str) or isinstance(user, unicode):
            user = user.getUserName()

        self._cache('anonymous').remove(user)
        self._cache('authenticated').remove(user)


    security.declareProtected(manage_users, 'isUnique')
    def isUnique(self, attr, value):
        """ 
            Find out if any objects have the same attribute value.
            This method should be called when a new user record is
            about to be created. It guards uniqueness of names by 
            warning for items with the same name.
        """
        search_str = filter_format('(%s=%s)', (attr, str(value)))
        res = self._delegate.search( base=self.users_base
                                   , scope=self.users_scope
                                   , filter=search_str
                                   )

        if res['exception']:
            return res['exception']

        return res['size'] < 1


    def getEncryptions(self):
        """ Return the possible encryptions """
        if not crypt:
            return ('SHA', 'SSHA', 'md5', 'clear')
        else:
            return ('crypt', 'SHA', 'SSHA', 'md5', 'clear')


    def getLog(self):
        """ Get the log for displaying """
        return self._log().getLog()


    security.declarePrivate('_log')
    def _log(self):
        """ Get the shared log resource """
        return getResource('%s-log' % self._hash, SimpleLog, ())


    security.declarePrivate('_cache')
    def _cache(self, cache_type='anonymous'):
        """ Get the specified user cache """
        return getResource( '%s-%scache' % (self._hash, cache_type)
                          , SimpleCache
                          , ()
                          )


    security.declarePrivate('_misc_cache')
    def _misc_cache(self):
        """ Return the miscellaneous cache """
        return getResource('%s-misc_cache' % self._hash, SharedObject, ())


    security.declareProtected(manage_users, 'getCacheTimeout')
    def getCacheTimeout(self, cache_type='anonymous'):
        """ Retrieve the cache timout value (in seconds) """
        if cache_type == 'authenticated':
            return getattr(self, '_authenticated_timeout', 600)
        else:
            return getattr(self, '_anonymous_timeout', 600)


    security.declareProtected(manage_users, 'setCacheTimeout')
    def setCacheTimeout( self
                       , cache_type='anonymous'
                       , timeout=600
                       , REQUEST=None
                       ):
        """ Set the cache timeout """
        if not timeout and timeout != 0:
            timeout = 600
        else:
            timeout = int(timeout)

        if cache_type == 'authenticated':
            self._authenticated_timeout = timeout
        elif cache_type == 'anonymous':
            self._anonymous_timeout = timeout

        self._cache(cache_type).setTimeout(timeout)

        if REQUEST is not None:
            msg = 'Cache timeout changed'
            return self.manage_cache(manage_tabs_message=msg)


    security.declareProtected(manage_users, 'getCurrentServer')
    def getCurrentServer(self):
        """ Simple UI Helper to show who we are currently connected to. """
        try:
            conn = self._delegate.connect()
        except:
            conn = None

        return getattr(conn, '_uri', '-- not connected --')


def manage_addLDAPUserFolder( self, title, LDAP_server, login_attr, uid_attr
                            , users_base, users_scope, roles, groups_base
                            , groups_scope, binduid, bindpwd, binduid_usage=1
                            , rdn_attr='cn', local_groups=0, use_ssl=0
                            , encryption='SHA', read_only=0, REQUEST=None
                            ):
    """ Called by Zope to create and install an LDAPUserFolder """
    this_folder = self.this()

    if hasattr(aq_base(this_folder), 'acl_users') and REQUEST is not None:
        msg = 'This+object+already+contains+a+User+Folder'

    else:
        n = LDAPUserFolder(
            title=title,
            LDAP_server=LDAP_server,
            login_attr=login_attr,
            uid_attr=uid_attr,
            users_base=users_base,
            users_scope=users_scope,
            roles=roles,
            groups_base=groups_base,
            groups_scope=groups_scope,
            binduid=binduid,
            bindpwd=bindpwd,
            binduid_usage=binduid_usage,
            rdn_attr=rdn_attr,
            local_groups=local_groups,
            use_ssl=not not use_ssl,
            encryption=encryption,
            read_only=read_only,
            REQUEST=None
            )

        this_folder._setObject('acl_users', n)
        this_folder.__allow_groups__ = self.acl_users

        # Try to set up an initial LDAP group to Zope role mapping
        # Any Zope role that has the same name as a group in LDAP
        # is automatically entered into the mapping. This creates
        # behavior simlar to the previous implicit mapping.
        ldap_groups = n.getGroups(attr='cn')
        valid_roles = this_folder.valid_roles()
        invalid_roles = ('Anonymous', 'Authenticated', 'Owner')

        for role in valid_roles:
            if role not in invalid_roles and role in ldap_groups:
                n.manage_addGroupMapping(role, role)

        msg = 'Added+LDAPUserFolder'
 
    # return to the parent object's manage_main
    if REQUEST:
        url = REQUEST['URL1']
        qs = 'manage_tabs_message=%s' % msg
        REQUEST.RESPONSE.redirect('%s/manage_main?%s' % (url, qs))

InitializeClass(LDAPUserFolder)
