"""
w2lapp.search.py: do a search and return results in several formats

web2ldap - a web-based LDAP Client,
see http://www.web2ldap.de for details

(c) by Michael Stroeder <michael@stroeder.com>

This module is distributed under the terms of the
GPL (GNU GENERAL PUBLIC LICENSE) Version 2
(see http://www.gnu.org/copyleft/gpl.html)

$Id: search.py,v 1.73 2002/02/02 14:14:35 michael Exp $
"""

import time,ldap,ldap.async,dsml, \
       msbase,pyweblib.forms,pyweblib.httphelper,ldaputil.base, \
       w2lapp.core,w2lapp.cnf,w2lapp.gui,w2lapp.searchform

from ldapurl import LDAPUrl

from msbase import intersection

SizeLimitMsg = """
<p>
  <strong>
    Only partial results received. Try to refine search.
  </strong><br>
  %s
</p>
"""

is_search_result = {
  'RES_SEARCH_ENTRY':None,
  'RES_SEARCH_RESULT':None
}

is_search_reference = {
  'RES_SEARCH_REFERENCE':None
}

class DSMLWriter(ldap.async.AsyncSearchHandler):
  """
  Class for writing a stream LDAP search results to a DSML file
  """
  _entryResultTypes = ldap.async._entryResultTypes

  def __init__(self,l,f,base64_attrs=[],dsml_comment=''):
    ldap.async.AsyncSearchHandler.__init__(self,l)
    self._dsmlWriter = dsml.DSMLWriter(
      f,base64_attrs,dsml_comment
    )
    self._base64_attrs = base64_attrs
    self._indent_offset = 2

  def preProcessing(self):
    self._dsmlWriter.writeHeader()

  def postProcessing(self):
    self._dsmlWriter.writeFooter()

  def _processSingleResult(self,resultType,resultItem):
    if self._entryResultTypes.has_key(resultType):
      dn,entry = resultItem
      self._dsmlWriter.writeRecord(dn,entry)


def w2l_Search(
  sid,outf,command,form,ls,dn,
  search_output = 'table',
  scope  = ldap.SCOPE_BASE,
  search_filterstr = None
):
  """
  Search for entries and output results as table, pretty-printable output
  or LDIF formatted
  """

  search_output = form.getInputValue('search_output',['table'])[0]

  if ('search_option' in form.inputFieldNames) or \
     ('search_attr' in form.inputFieldNames) or \
     ('search_string' in form.inputFieldNames):
    search_mode = form.getInputValue('search_mode',[r'(&%s)'])[0]
    search_option = form.getInputValue('search_option',[])
    search_attr = form.getInputValue('search_attr',[])
    search_string = form.getInputValue('search_string',[])
    if (len(search_option)==len(search_attr)==len(search_string)):
      # Get search mode
      # Build LDAP search filter from input data of advanced search form
      search_filter = []
      for i in range(len(form.field['search_option'].value)):
        search_string,search_option,search_attr = \
          form.field['search_string'].value[i], \
          form.field['search_option'].value[i], \
          form.field['search_attr'].value[i]
        if search_option==r'(%s=*)':
          search_filter.append(search_option % (search_attr))
        elif search_string:
          search_filter.append(search_option % (
    	    search_attr,ldaputil.base.escape_filter_chars(search_string)
	  ))
      # Teilsuchen werden alle und-verknuepft
      if len(search_filter)==1:
        search_mode = '%s'
      search_filterstr = search_mode % (''.join(search_filter))
    else:
      raise w2lapp.core.ErrorExitClass(ls,dn,'Invalid search form data.')
  elif 'search_filterstr' in form.inputFieldNames:
    search_filterstr = form.getInputValue(
      'search_filterstr',['(objectClass=*)']
    )[0]

  search_resminindex = int(
    form.getInputValue('search_resminindex',['0'])[0]
  )
  search_resnumber = int(
    form.getInputValue(
      'search_resnumber',
      [str(w2lapp.cnf.GetParam(ls,'search_resultsperpage',10))]
    )[0]
  )

  if search_output=='print':
    print_template_filenames_dict = w2lapp.cnf.GetParam(ls,'print_template',None)
    print_cols = w2lapp.cnf.GetParam(ls,'print_cols','4')
    
    if print_template_filenames_dict is None:
      raise w2lapp.core.ErrorExitClass(ls,dn,'No print templates defined.')

    read_attrs = ['objectclass']
    print_template_str_dict = msbase.CaseinsensitiveStringKeyDict()

    for oc in print_template_filenames_dict.keys():
      try:
	print_template_str_dict[oc] = open(print_template_filenames_dict[oc],'r').read()
      except IOError:
	pass
      else:
        read_attrs = msbase.union(
	  read_attrs,
	  msbase.GrabKeys(print_template_str_dict[oc]).keys
	)
    result_handler = ldap.async.List(ls.l)

  elif search_output=='table':

    search_tdtemplate = msbase.CaseinsensitiveStringKeyDict(
      w2lapp.cnf.GetParam(ls,'search_tdtemplate',{})
    )
    search_tdtemplate_keys = search_tdtemplate.keys()

    # Build the list of attributes to read
    read_attrs = []
    for oc in search_tdtemplate_keys:
      read_attrs = msbase.union(
	read_attrs,
	msbase.GrabKeys(search_tdtemplate[oc]).keys
      )

    search_tablistattrs = w2lapp.cnf.GetParam(ls,'search_tablistattrs',[])
    if search_tablistattrs:
      read_attrs.extend(search_tablistattrs)
    read_attrs.extend([
      'objectClass','displayName','hasSubordinates','subordinateCount'
    ])
    result_handler = ldap.async.List(ls.l)

  elif search_output in ['ldif','ldif1']:
    # read all attributes
    read_attrs = []
    result_handler = ldap.async.LDIFWriter(ls.l,outf)

  elif search_output=='dsml':
    # read all attributes
    read_attrs = []
    result_handler = DSMLWriter(
      ls.l,outf,base64_attrs=w2lapp.core.ldap_binaryattrkeys
    )

  search_ldap_url = LDAPUrl(
    hostport=ls.host,
    dn=dn,
    attrs=read_attrs,
    scope=scope,
    filterstr=search_filterstr,
  )

  try:
    # Start the search
    result_handler.startSearch(
      dn.encode('utf-8'),
      scope,
      search_filterstr.encode('utf-8'),
      ldaputil.base.encode_unicode_list(read_attrs),
      0
    )
  except ldap.FILTER_ERROR:
    # Give the user a chance to edit his bad search filter
    w2lapp.searchform.w2l_SearchForm(
      sid,outf,command,form,ls,dn,searchform_mode='exp',
      Msg='<p>Error: Bad search filter!</p>',
      search_filterstr=search_filterstr,
      scope=scope
    )
    return
  except ldap.NO_SUCH_OBJECT,e:
    if dn:
      raise e

  if search_output=='table':

    search_maxhits = search_resminindex+search_resnumber
    
    SearchWarningMsg = ''

    try:
      partial_results = result_handler.processResults(
        search_resminindex,search_resnumber
      )
    except (ldap.SIZELIMIT_EXCEEDED,ldap.ADMINLIMIT_EXCEEDED),e:
      partial_results = 0
      SearchWarningMsg = SizeLimitMsg % (w2lapp.gui.LDAPError2ErrMsg(e))
    except ldap.NO_SUCH_OBJECT,e:
      partial_results = 0
      if dn:
        raise e

    search_resminindex = result_handler.beginResultsDropped
    resind = result_handler.endResultBreak
    result_dnlist = result_handler.allResults

    # HACK! If searching the root level returns empty search results
    # the namingContexts is queried
    if not dn and scope==ldap.SCOPE_ONELEVEL:
      result_dnlist.extend([
        ('RES_SEARCH_ENTRY',(dn.encode(ls.charset),{}))
        for dn in ls.namingContexts
      ])

    result_dnlist.sort()

    ContextMenuList = []

    if result_dnlist:

      # There's still data left to be read
      if partial_results or search_resminindex:

        prev_resminindex = max(0,search_resminindex-search_resnumber)

        if search_resminindex+search_resnumber<=resind:
          ContextMenuList.append(
	    w2lapp.gui.W2L_Anchor(
	      form,'search','-&gt;&gt;',sid,
              [
                ('dn',dn),
                ('search_filterstr',search_filterstr),
                ('search_resminindex',str(search_resminindex+search_resnumber)),
                ('search_resnumber',str(search_resnumber)),
                ('scope',str(scope)),
              ]
            )
	  )

        if search_resminindex:
          ContextMenuList.append(
	    w2lapp.gui.W2L_Anchor(
	      form,'search','&lt;&lt;-',sid,
              [
                ('dn',dn),
                ('search_filterstr',search_filterstr),
                ('search_resminindex',str(prev_resminindex)),
                ('search_resnumber',str(search_resnumber)),
                ('scope',str(scope)),
              ]
            )
	  )

        ContextMenuList.append(
	  w2lapp.gui.W2L_Anchor(
	    form,'search','Display all',sid,
            [
              ('dn',dn),
              ('search_filterstr',search_filterstr),
              ('search_resnumber','0'),
              ('scope',str(scope)),
            ]
          )
        )

	result_message = 'Results %d - %d of %s search with filter &quot;%s&quot;.' % (
	    search_resminindex+1,
	    resind,
	    ldaputil.base.SEARCH_SCOPE_STR[scope],
	    w2lapp.core.utf2display(form.accept_charset,search_filterstr)
        )

      else:
	result_message = '%s search with filter &quot;%s&quot; found %d entries.' % (
	  ldaputil.base.SEARCH_SCOPE_STR[scope],
	  w2lapp.core.utf2display(form.accept_charset,search_filterstr),
	  len(result_dnlist)
	)

      ContextMenuList.extend([
	w2lapp.gui.W2L_Anchor(
	  form,'searchform','Refine Filter',sid,
          [
            ('dn',dn),
            ('searchform_mode','exp'),
            ('search_root',dn),
            ('search_filterstr',search_filterstr),
            ('scope',str(scope)),
          ],
        ),
	w2lapp.gui.W2L_Anchor(
	  form,'search','Printable',sid,
          [
            ('dn',dn),
            ('search_output','print'),
            ('scope',str(scope)),
            ('search_filterstr',search_filterstr),
          ],
	  target='_altoutput'
	),
	w2lapp.gui.W2L_Anchor(
	  form,'search','LDIF',sid,
          [
            ('dn',dn),
            ('search_output','ldif'),
            ('scope',str(scope)),
            ('search_filterstr',search_filterstr),
          ],
	  target='_altoutput'
	),
	w2lapp.gui.W2L_Anchor(
	  form,'search','LDIFv1',sid,
          [
            ('dn',dn),
            ('search_output','ldif1'),
            ('scope',str(scope)),
            ('search_filterstr',search_filterstr),
          ],
	  target='_altoutput'
	),
	w2lapp.gui.W2L_Anchor(
	  form,'search','DSML',sid,
          [
            ('dn',dn),
            ('search_output','dsml'),
            ('scope',str(scope)),
            ('search_filterstr',search_filterstr),
          ],
	  target='_altoutput'
	),
      ])

      w2lapp.gui.TopSection(
        sid,outf,form,ls,dn,
        'Search Results',
        w2lapp.gui.MainMenu(sid,form,ls,dn),
        context_menu_list=ContextMenuList
      )

      outf.write('<div id="MessageDiv">%s\n<p>%s\n%s</p>\n' % (
          SearchWarningMsg,result_message,
          search_ldap_url.htmlHREF(
              httpCharset=form.accept_charset,
              hrefText='Search by LDAP URL',
              hrefTarget='_ldapurl',
            )
        )
      )

      mailtolist = []
      for r in result_dnlist:
        if is_search_result.has_key(r[0]):
          mailtolist.extend(
            r[1][1].get(
              'mail',
              r[1][1].get('rfc822Mailbox',[])
            )
          )
      if mailtolist:
	outf.write(
          '- <a href="mailto:%s?cc=%s">Mail to all Cc:-ed</a>- <a href="mailto:?bcc=%s">Mail to all Bcc:-ed</a>' % (
	    mailtolist[0],
	    ','.join(mailtolist[1:]),
	    ','.join(mailtolist)
  	  )
  	)

      outf.write('<dl class="SearchResultList">\n')

      for r in result_dnlist:

        if is_search_reference.has_key(r[0]):

          # Display a search continuation (search reference)
          entry = {}
          refUrl = LDAPUrl(r[1][1][0])
          host,dn = refUrl.hostport,refUrl.dn
          result_dd_str='Search reference =&gt; %s' % (
            refUrl.htmlHREF(
              httpCharset=form.accept_charset,
              hrefTarget='_ldapurl'
            )
          )
          command_table = [
            w2lapp.gui.W2L_Anchor(
	      form,'search','Down',{0:sid,1:None}[host!=ls.host],
              [('host',host),('dn',dn),('scope',str(ldap.SCOPE_ONELEVEL))]
            ),
            w2lapp.gui.W2L_Anchor(
	      form,'read','Read',{0:sid,1:None}[host!=ls.host],
              [('host',host),('dn',dn)]
	    )
          ]

        elif is_search_result.has_key(r[0]):

          # Display a search result with entry's data
          dn,entry = r[1]
          objectclasses = entry.get('objectClass',entry.get('objectclass',[]))
	  tdtemplate_oc = intersection(
	    objectclasses,
	    search_tdtemplate_keys,
            ignorecase=1
	  )

          if entry.has_key('displayName') or entry.has_key('displayname'):
            display_name = unicode(
              entry.get(
                'displayName',
                entry.get(
                  'displayname',''
                )
              )[0],
              ls.charset
            )
            result_dd_str=display_name.encode(form.accept_charset)

          elif tdtemplate_oc:

            template_attrs = []
	    for oc in tdtemplate_oc:
	      template_attrs.extend(msbase.GrabKeys(search_tdtemplate[oc]).keys)
	    tableentry_attrs = intersection(
	      template_attrs,
	      entry.keys(),
	      ignorecase=1
	    )
            if tableentry_attrs:
              # Output entry with the help of pre-defined templates
	      tableentry = msbase.CaseinsensitiveStringKeyDict(default='&nbsp;')
	      for attr in tableentry_attrs:
	        tableentry[attr] = []
	        for value in entry[attr]:
  		  tableentry[attr].append(
                    w2lapp.gui.DataStr(sid,form,ls,dn,attr,value,commandbutton=0)
                  )
	        tableentry[attr] = ', '.join(tableentry[attr])
              tdlist = []
	      for oc in tdtemplate_oc:
	        tdlist.append(search_tdtemplate[oc] % tableentry)
	      result_dd_str='<br>\n'.join(tdlist)
	    else:
              # Output DN
	      result_dd_str=w2lapp.core.utf2display(form.accept_charset,dn)

	  elif search_tablistattrs and entry.has_key(search_tablistattrs[0]):
            # Output values of attributes listed in search_tablistattrs
	    tableentry = msbase.CaseinsensitiveStringKeyDict(default='')
	    tableentry_attrs = intersection(
	      search_tablistattrs,
	      entry.keys(),
	      ignorecase=1
	    )
	    for attr in tableentry_attrs:
	      tableentry[attr] = []
	      for value in entry[attr]:
  	        tableentry[attr].append(
                  w2lapp.gui.DataStr(sid,form,ls,dn,attr,value,commandbutton=0)
                )
	      tableentry[attr] = '<br>'.join(tableentry[attr])
	    tdlist = []
	    for attr in search_tablistattrs:
	      tdlist.append(tableentry[attr])
            result_dd_str='<br>\n'.join(filter(None,tdlist))

	  else:
            # Output DN
	    result_dd_str=w2lapp.core.utf2display(form.accept_charset,dn)

          # Build the list for link table
          command_table = []
          # Try to determine from entry's attributes if there are subordinates
          hasSubordinates = entry.get('hasSubordinates',['TRUE'])[0]=='TRUE'
          subordinateCount = int(entry.get('subordinateCount',['1'])[0])
          # If subordinates or unsure a [Down] link is added
          if hasSubordinates and subordinateCount>0:
            command_table.append(
              w2lapp.gui.W2L_Anchor(
	        form,'search','Down',sid,
                [('dn',dn),('scope',str(ldap.SCOPE_ONELEVEL))]
              )
            )
          # A [Read] link is added in any case
          command_table.append(
            w2lapp.gui.W2L_Anchor(
	      form,'read','Read',sid,[('dn',dn)]
	    ),
          )

        else:
          raise ValueError,"LDAP result of invalid type %s." % (r[0])

	outf.write('<dt>\n%s\n</dt>\n' % (
            ' | \n'.join(command_table)
          )
        )
	outf.write('<dd class="SearchResultItem">\n%s\n</dd>\n' % (result_dd_str))
      outf.write('</dl></div>')

      w2lapp.gui.PrintFooter(outf,form)

    else:
    
      ##############################
      # Empty search results
      ##############################

      ContextMenuList = [
	w2lapp.gui.W2L_Anchor(
	  form,'searchform','Refine Filter',sid,
          [
            ('search_root',dn),
            ('searchform_mode','exp'),
            ('search_filterstr',search_filterstr),
            ('scope',str(scope)),
          ],
        )
      ]

      w2lapp.gui.SimpleMessage(
        sid,outf,form,ls,dn,
        'No Search Results',
        'No Entries found with %s search with filter &quot;%s&quot;.' % (
          ldaputil.base.SEARCH_SCOPE_STR[scope],
          w2lapp.core.utf2display(form.accept_charset,search_filterstr)
        ),
        main_menu_list=w2lapp.gui.MainMenu(sid,form,ls,dn),
        context_menu_list=ContextMenuList
      )

  elif search_output=='print':

    result_handler.processResults()
    result_handler.allResults.sort()

    table=[]
    for r in result_handler.allResults:
      if r[0] in ['RES_SEARCH_ENTRY','RES_SEARCH_RESULT']:
        dn = unicode(r[1][0],ls.charset)
        entry = r[1][1]
        objectclasses = entry.get('objectclass',entry.get('objectClass',[]))
        template_oc = intersection(
          [o.lower() for o in objectclasses],
	  [s.lower() for s in print_template_str_dict.keys()]
        )
        if template_oc:
	  tableentry = msbase.DefaultDict('')
	  attr_list=entry.keys()
	  for attr in attr_list:
	    tableentry[attr] = ', '.join(w2lapp.core.utf2display(form.accept_charset,entry[attr]))
	  table.append(print_template_str_dict[template_oc[0]] % (tableentry))

    # Output search results as pretty-printable table without buttons
    w2lapp.gui.PrintHeader(outf,'Pretty-Printable Search Results',form.accept_charset)
    outf.write("""<h1>%s</h1>
<table
  border="1"
  rules="rows"
  id="PrintTable"
  summary="Table with search results formatted for printing">
    <colgroup span="%d">
    """ % (w2lapp.core.utf2display(form.accept_charset,dn),print_cols))
    tdwidth=100/print_cols
    for i in range(print_cols):
      outf.write('<col width="%d%%">\n' % (tdwidth))
    outf.write('</colgroup>\n')
    for i in range(len(table)):
      if i%print_cols==0:
        outf.write('<tr>')
      outf.write('<td width="%d%%">%s</td>' % (tdwidth,table[i]))
      if i%print_cols==print_cols-1:
        outf.write('</tr>\n')
    if len(table)%print_cols!=print_cols-1:
      outf.write('</tr>\n')

    outf.write('</table>\n')

    w2lapp.gui.PrintFooter(outf,form)


  ################################################################
  # Output LDIF or LDIF 1.0
  ################################################################

  elif search_output in ['ldif','ldif1']:

    # Output search results as LDIF
    pyweblib.httphelper.SendHeader(outf,'text/plain',charset='US-ASCII')
    if search_output=='ldif1':
      result_handler.headerStr = """########################################################################
# LDIF export by web2ldap %s, see http://www.web2ldap.de
# Date and time: %s
# Bind-DN: %s
# LDAP-URL of search:
# %s
########################################################################
version: 1

""" % (
      w2lapp.__version__,
      time.strftime(
        '%A, %Y-%m-%d %H:%M:%S GMT',
        time.gmtime(time.time())
      ),
      repr(ls.who),str(search_ldap_url)
    )
    result_handler.processResults()


  ################################################################
  # Output DSML
  ################################################################

  elif search_output=='dsml':

    # Output search results as DSML in a rather primitive way.
    # (level 1 producer)
    pyweblib.httphelper.SendHeader(outf,'text/xml',charset='UTF-8')
    result_handler._dsmlWriter._dsml_comment = ('\n'+result_handler._dsmlWriter._indent).join([
        'DSML exported by web2ldap %s, see http://www.web2ldap.de' % (w2lapp.__version__),
        'Date and time: %s' % (time.strftime('%A, %Y-%m-%d %H:%M:%S GMT',time.gmtime(time.time()))),
        'Bind-DN: %s' % (repr(ls.who)),
        'LDAP-URL of search:',
        str(search_ldap_url),
    ])
    result_handler.processResults()
