# -*- coding: utf-8 -*-
# core of LDAP Filter Methods.
 
 
__version__ = "$Revision: 1.10 $"[11:-2]
 
try:
  import ldap
  from ldap import modlist
# see if it's on a regular path
except ImportError:
  from Products.ZLDAPConnection import ldap
  from Products.ZLDAPConnection.ldap import modlist
import string
 
from Shared.DC.ZRDB import Aqueduct
from Shared.DC.ZRDB.Aqueduct import parse, decodestring, default_input_form
from Shared.DC.ZRDB.Results import Results
import Acquisition, AccessControl.Role, OFS.SimpleItem
from App.special_dtml import HTMLFile
from App.Dialogs import MessageDialog
from Persistence import Persistent
import DocumentTemplate
import ExtensionClass
import sys
 
from zLOG import LOG, INFO
from ldif import LDIFRecordList, is_dn, valid_changetype_dict, CHANGE_TYPES
import ldifvar
try:
  from DocumentTemplate.security import RestrictedDTML
except ImportError: # BBB Zope-2.12
  from AccessControl.DTML import RestrictedDTML
try:
    from AccessControl import getSecurityManager
except ImportError:
    getSecurityManager = None
 
MODIFY_MAPPING_DICT = {'add'      : ldap.MOD_ADD,
                       #'replace'  : ldap.MOD_REPLACE,
                       'delete'   : ldap.MOD_DELETE}
 
class ERP5LDIFRecordList(LDIFRecordList):
 
  def parse(self):
    """
    Continously read and parse LDIF records
    """
    self._line = self._input_file.readline()
 
    while self._line and \
          (not self._max_entries or self.records_read<self._max_entries):
 
      # Reset record
      version = None; dn = None; changetype = None; modop = None; entry = {};
 
      attr_type,attr_value = self._parseAttrTypeandValue()
 
      while attr_type!=None and attr_value!=None:
        if attr_type=='dn':
          # attr type and value pair was DN of LDIF record
          if dn!=None:
            raise ValueError, 'Two lines starting with dn: in one record.'
          if not is_dn(attr_value):
            raise ValueError, 'No valid string-representation of distinguished name %s.' % (repr(attr_value))
          dn = attr_value
        elif attr_type=='version' and dn is None:
          version = 1
        elif attr_type=='changetype':
          # attr type and value pair was DN of LDIF record
          if dn is None:
            raise ValueError, 'Read changetype: before getting valid dn: line.'
          if changetype!=None:
            raise ValueError, 'Two lines starting with changetype: in one record.'
          if not valid_changetype_dict.has_key(attr_value):
            raise ValueError, 'changetype value %s is invalid.' % (repr(attr_value))
          changetype = attr_value
          attr_type, attr_value = self._parseAttrTypeandValue()
          modify_list = []
          entry[changetype] = []
          while (attr_type and attr_value) is not None:
            mod_op = MODIFY_MAPPING_DICT[attr_type]
            mod_type = attr_value
            multivalued_list = []
            while attr_value is not None:
              attr_type, attr_value = self._parseAttrTypeandValue()
              if attr_value is not None:
                multivalued_list.append(attr_value)
            modify_list.append((mod_op, mod_type, multivalued_list))
            entry[changetype] = [modify_list]
            attr_type, attr_value = self._parseAttrTypeandValue()
          #don't add new entry for the same dn
          break
        elif attr_value not in (None, '') and \
             not self._ignored_attr_types.has_key(string.lower(attr_type)):
          # Add the attribute to the entry if not ignored attribute
          if entry.has_key(attr_type):
            entry[attr_type].append(attr_value)
          else:
            entry[attr_type]=[attr_value]
 
        # Read the next line within an entry
        attr_type, attr_value = self._parseAttrTypeandValue()
 
      if entry:
        # append entry to result list
        self.handle(dn, entry)
        self.records_read = self.records_read+1
 
    return # parse()
 
class Filter(DocumentTemplate.HTML):
    """
    Subclass of DocumentTemplate.HTML for Variable Interpolation.
    Special LDAP Specific tags would go here.  Since there aren't
    any (like ldapvar or ldaptest or whatever), we don't have to worry.
    It's just nice to have a nice name that reflects what this is. :)
    """
    pass
 
class nvLDIF(DocumentTemplate.HTML):
    # Non-validating Ldif Template for use by LDIFFiles.
    commands={}
    for k, v in DocumentTemplate.HTML.commands.items(): commands[k] = v
    commands['ldifvar' ] = ldifvar.LDIFVar
    commands['ldifline' ] = ldifvar.LDIFLine
 
    _proxy_roles=()
 
class Ldif(RestrictedDTML, ExtensionClass.Base, nvLDIF):
    pass
 
def LDAPConnectionIDs(self):
    """find LDAP connections in the current folder and parents
    Returns list of ids.
    """
    ids={}
    StringType = type('')
    have_key = ids.has_key
    while self is not None:
        if hasattr(self, 'objectValues'):
            for o in self.objectValues():
                if (hasattr(o,'_isAnLDAPConnection')
                    and o._isAnLDAPConnection() and hasattr(o,'id')):
                    id=o.id
                    if type(id) is not StringType: id=id()
                    if not ids.has_key(id):
                        if hasattr(o,'title_and_id'): o=o.title_and_id()
                        else: o=id
                        ids[id]=id
        if hasattr(self, 'aq_parent'): self=self.aq_parent
        else: self=None
    ids=map(lambda item: (item[1], item[0]), ids.items())
    ids.sort()
    return ids
 
manage_addZLDAPMethodForm = HTMLFile('add', globals())
 
def manage_addZLDAPMethod(self, id, title, connection_id, scope, basedn, 
                          filters, arguments, getfromconnection=0,
                          REQUEST=None, submit=None):
    """Add an LDAP Method """
    l=LDAPMethod(id, title, connection_id, scope, basedn,
                 arguments, filters)
    self._setObject(id, l)
    if getfromconnection:
        getattr(self,id).recomputeBaseDN()
 
    if REQUEST is not None:
        u=REQUEST['URL1']
        if submit==" Add and Edit ":
            u="%s/%s/manage_main" % (u,id)
        elif submit==" Add and Test ":
            u="%s/%s/manage_testForm" % (u,id)
        else:
            u=u+'/manage_main'
 
        REQUEST.RESPONSE.redirect(u)
    return ''
 
 
 
 
_ldapScopes = { "ONELEVEL": ldap.SCOPE_ONELEVEL,
                "SUBTREE": ldap.SCOPE_SUBTREE,
                "BASE": ldap.SCOPE_BASE }
 
class LDAPError(Exception):
  pass
 
class LDAPMethod(Aqueduct.BaseQuery,
    Acquisition.Implicit,
    Persistent,
    AccessControl.Role.RoleManager,
    OFS.SimpleItem.Item,
    ):
    'LDAP Method'
 
    meta_type = 'LDAP Method'
 
    manage_main = HTMLFile('edit', globals())
    manage_options = (
        {'label':'Edit', 'action':'manage_main'},
        {'label':'Test', 'action':'manage_testForm'},
        {'label':'Security', 'action':'manage_access'},
        )
 
    __ac_permissions__=(
        ('View management screens', ('manage_tabs','manage_main',),),
        ('Change LDAP Methods', ('manage_edit',
                                 'manage_testForm','manage_test')),
        ('Use LDAP Methods',    ('__call__',''), ('Anonymous','Manager')),
        )
 
 
    #manage_testForm = HTMLFile("testForm", globals())
 
    def manage_testForm(self, REQUEST):
        " "
        input_src=default_input_form(self.title_or_id(),
                                     self._arg, 'manage_test',
                                     '<!--#var manage_tabs-->')
        return DocumentTemplate.HTML(input_src)(self,REQUEST,HTTP_REFERER='')
 
    def __init__(self, id, title, connection_id, scope, basedn,
                 arguments, filters):
        """ init method """
        self.id = id
        self.title = title
        self.connection_id = connection_id
        self._scope = _ldapScopes[scope]
        self.scope = scope
        self.basedn = basedn
        self.arguments_src=self.arguments=arguments
        self._arg=parse(arguments)
        self.filters = filters
 
    def recomputeBaseDN(self):
        ' recompute base DN based on connection '
        cdn=self._connection().dn
        if self.basedn:
            self.basedn='%s, %s' % (self.basedn, cdn)
        else:
            self.basedn=cdn
        return self.basedn
 
    def manage_edit(self, title, connection_id, scope, basedn,
                    arguments, filters, REQUEST=None):
        """ commit changes """
        self.title = title
        self.connection_id = connection_id
        self._scope = _ldapScopes[scope]
        self.scope = scope
        self.basedn = basedn
        self.arguments_src=self.arguments=arguments
        self._arg=parse(arguments)
        self.filters = filters
        if REQUEST is not None:
            return MessageDialog(
                title='Edited',
                message='<strong>%s</strong> has been changed.' % self.id,
                action ='./manage_main', )
 
    def cleanse(self,s):
        import string
        # kill line breaks &c.
        s = string.join(string.split(s))
        return s
 
    def _connection(self):
        ' return actual ZLDAP Connection Object '
        return getattr(self, self.connection_id, None)
 
    def _getConn(self):
        return self._connection().GetConnection()
 
    # Hacky, Hacky
    GetConnection=_getConn
 
    def manage_test(self, REQUEST):
        """ do the test query """
        src="Could not render the filter template!"
        res=()
        t=v=tb=None
        try:
            try:
                src=self(REQUEST, src__=1)
                res=self(REQUEST, tst__=1)
                r=self.prettyResults(res)
            except:
                t, v, tb = sys.exc_info()
                r='<strong>Error, <em>%s</em>:</strong> %s' % (t,v)
 
            report=DocumentTemplate.HTML(
                '<html><body bgcolor="#ffffff">\n'
                '<!--#var manage_tabs-->\n<hr>%s\n\n'
                '<hr><strong>Filter used:</strong><br>\n<pre>\n%s\n</pre>\n<hr>\n'
                '</body></html>' % (r, src)
                )
            report=apply(report,(self,REQUEST),{self.id:res})
 
            if tb is not None:
                self.raise_standardErrorMessage(
                    None, REQUEST, t, v, tb, None, report)
 
            return report
 
        finally: tb=None
 
    def prettyResults(self, res):
        s = ""
        if not res or not len(res):
            s = "no results"
        else:
            for dn,attrs in res:
                s = s + ('<ul><li><b>DN: %s</b></li>\n<ul>' % dn)
                s = s + str(pretty_results(attrs=attrs.items()))
                s = s + '</ul></ul>'
        return s
 
    def __call__(self, REQUEST=None, src__=0, tst__=0, **kw):
        """ call the object """
        if REQUEST is None:
            if kw: REQUEST = kw
            else:
                if getattr(self, 'REQUEST', None) is not None: REQUEST=self.REQUEST
                else: REQUEST={}
        c = self._getConn()
        if not c:
            raise LDAPError('LDAP Connection not open')
 
        if getattr(self, 'aq_parent', None) is not None:
            p = self.aq_parent
        else: p = None
 
        argdata = self._argdata(REQUEST)  #use our BaseQuery's magic.  :)
 
        # Also need the authenticated user.
        auth_user = REQUEST.get('AUTHENTICATED_USER', None)
        if auth_user is None:
            auth_user = getattr(self, 'REQUEST', None)
            if auth_user is not None:
                try: auth_user = auth_user.get('AUTHENTICATED_USER', None)
                except: auth_user = None
 
        if auth_user is not None:
            if getSecurityManager is None:
                # working in a pre-Zope 2.2.x instance
                from AccessControl.User import verify_watermark
                verify_watermark(auth_user)
                argdata['AUTHENTICATED_USER'] = auth_user
 
        f = Filter(self.filters)        # make a FilterTemplate
        f.cook()
        if getSecurityManager is None:
            # working in a pre-Zope 2.2 instance
            f = apply(f, (p,argdata))       #apply the template
        else:
            # Working with the new security manager (Zope 2.2.x ++)
            security = getSecurityManager()
            security.addContext(self)
            try:     f = apply(f, (p,), argdata)  # apply the template
            finally: security.removeContext(self)
 
        f = str(f)                      #ensure it's a string
        if src__: return f              #return the rendered source
        f = self.cleanse(f)
        ### run the search
        res = c.search_s(self.basedn, self._scope, f)
        if tst__: return res            #return test-friendly data
 
        ### instantiate Entry objects based on results
        l = []                          #list of entries to return
        conn=self._connection()         #ZLDAPConnection
        Entry = conn._EntryFactory()
        for dn, attrdict in res:
            e = Entry(dn, attrdict, conn).__of__(self)
            l.append(e)
 
        return l
 
 
 
manage_addZLDIFMethodForm = HTMLFile('addLdif', globals())
 
def manage_addZLDIFMethod(self, id, title, connection_id, basedn, arguments, ldif, getfromconnection=0, REQUEST=None, submit=None):
  """Add an LDIF Method """
  l=LDIFMethod(id, title, connection_id, basedn, arguments, ldif)
  self._setObject(id, l)
  if getfromconnection:
    getattr(self,id).recomputeBaseDN()
 
  if REQUEST is not None:
    u=REQUEST['URL1']
    if submit == " Add and Edit ":
        u = "%s/%s/manage_main" % (u, id)
    elif submit == " Add and Test ":
        u = "%s/%s/manage_testForm" % (u, id)
    else:
        u = u + '/manage_main'
 
    REQUEST.RESPONSE.redirect(u)
  return ''
 
 
class LDIFMethod(LDAPMethod):
  'LDIF Method'
 
  meta_type = 'LDIF Method'
 
  manage_main = HTMLFile('editLdif', globals())
  manage_options = (
      {'label':'Edit', 'action':'manage_main'},
      {'label':'Test', 'action':'manage_testForm'},
      {'label':'Security', 'action':'manage_access'},
      )
 
  __ac_permissions__=(
      ('View management screens', ('manage_tabs', 'manage_main',),),
      ('Change LDAP Methods', ('manage_edit',
                                'manage_testForm', 'manage_test')),
      ('Use LDAP Methods',    ('__call__', ''), ('Anonymous', 'Manager')),
      )
 
  #manage_testForm = HTMLFile("testLdifForm", globals())
 
 
 
  def __init__(self, id, title, connection_id, basedn, arguments, ldif, **kw):
    """ init method """
    self.id = id
    self.title = title
    self.connection_id = connection_id
    self.basedn = basedn
    self.arguments_src=self.arguments=arguments
    self._arg=parse(arguments)
    self.ldif = str(ldif)
 
 
  def manage_edit(self, title, connection_id, basedn, arguments, ldif, REQUEST=None, **kw):
    """ commit changes """
    self.title = title
    self.connection_id = connection_id
    self.basedn = basedn
    self.arguments_src = self.arguments = arguments
    self._arg = parse(arguments)
    self.ldif = str(ldif)
    if REQUEST is not None:
      return MessageDialog(
        title = 'Edited',
        message = '<strong>%s</strong> has been changed.' % self.id,
        action = './manage_main', )
 
  def __call__(self, REQUEST=None, src__=0, tst__=0, **kw):
    """ call the object """
    if REQUEST is None:
      if kw: REQUEST = kw
      else:
        if hasattr(self, 'REQUEST'): REQUEST = self.REQUEST
        else: REQUEST={}
    c = self._connection().GetConnection()
    if not c:
      raise LDAPError('LDAP Connection not open')
 
    if hasattr(self, 'aq_parent'):
      p = self.aq_parent
    else: p = None
 
    argdata = self._argdata(REQUEST)  #use our BaseQuery's magic.  :)
    argdata['basedn'] = self.basedn
    # Also need the authenticated user.
    auth_user = REQUEST.get('AUTHENTICATED_USER', None)
    if auth_user is None:
      auth_user = getattr(self, 'REQUEST', None)
      if auth_user is not None:
        try: auth_user = auth_user.get('AUTHENTICATED_USER', None)
        except: auth_user = None
 
    if auth_user is not None:
      if getSecurityManager is None:
        # working in a pre-Zope 2.2.x instance
        from AccessControl.User import verify_watermark
        verify_watermark(auth_user)
        argdata['AUTHENTICATED_USER'] = auth_user
 
    ldif = Ldif(self.ldif)        # make a FilterTemplate
    ldif.cook()
    if getSecurityManager is None:
      # working in a pre-Zope 2.2 instance
      ldif = apply(ldif, (p, argdata))       #apply the template
    else:
      # Working with the new security manager (Zope 2.2.x ++)
      security = getSecurityManager()
      security.addContext(self)
      try:     ldif = apply(ldif, (p,), argdata)  # apply the template
      finally: security.removeContext(self)
 
    ldif = str(ldif)                      #ensure it's a string
    #LOG('ldif', 0, ldif)
    if src__: return ldif              #return the rendered source
    ### Apply Query
    from cStringIO import StringIO
    file = StringIO(ldif)
    l = ERP5LDIFRecordList(file)
    l.parse()
    res = l.all_records
 
    def delete(c, dn):
      try:
        c.delete_s(dn)
      except ldap.NO_SUCH_OBJECT:
        pass
      except:
        LOG('ldif', INFO, ldif)
        raise
 
    def add(c, dn, mod_list):
      try:
        c.add_s(dn, mod_list)
      except ldap.ALREADY_EXISTS:
        pass
      except:
        LOG('ldif', INFO, ldif)
        raise
 
    for record in res:
      dn = record[0]
      entry = record[1]
      if type(entry) == type({}):
        authorized_modify_key = [key for key in entry.keys() if key in CHANGE_TYPES]
        if len(authorized_modify_key):
          for key in authorized_modify_key:
            tuple_list = entry[key]
            if key == 'delete':
              try:
                delete(c, dn)
              except ldap.SERVER_DOWN:
                c = self._connection().getForcedConnection()
                delete(c, dn)
            else:
              for mod_tuple in tuple_list:
                c.modify_s(dn, mod_tuple)
        else:
          mod_list = modlist.addModlist(entry)
          try:
            add(c, dn, mod_list)
          except ldap.SERVER_DOWN:
            c = self._connection().getForcedConnection()
            add(c, dn, mod_list)
      else:
        LOG('LDIFMethod Type unknow', INFO, '')
    return res
 
 
class LDAP(LDAPMethod):
    "backwards compatibility.  blech. XXX Delete Me!"
 
pretty_results=DocumentTemplate.HTML("""\
  <table border="1" cellpadding="2" cellspacing="0" rules="rows" frame="void">
   <dtml-in attrs>
    <tr valign="top">
     <th align="left">&dtml-sequence-key;</th>
     <td><dtml-in name="sequence-item">&dtml-sequence-item;<br /></dtml-in></td>
    </tr>
   </dtml-in>
  </table>""")
 
 
import App.class_init
App.class_init.default__class_init__(LDAPMethod)
App.class_init.default__class_init__(LDIFMethod)