# -*- coding: utf-8 -*-
"""
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)
"""

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


from ldapurl import LDAPUrl
from ldaputil.base import escape_ldap_filter_chars
from msbase import GrabKeys
from w2lapp.schema.syntaxes import syntax_registry
from ldaputil.controls import SearchNoOpControl
from types import StringType,UnicodeType
from ldapsession import LDAPLimitErrors

SEARCH_NOOP_TIMEOUT = 5.0

SizeLimitMsg = """
<p class="ErrorMessage">
  <strong>
    Only partial results received. Try to refine search.
  </strong><br>
  {error_msg}
</p>
"""

is_search_result = ldap.async._entryResultTypes

is_search_reference = {
  ldap.RES_SEARCH_REFERENCE:None
}


class excel_semicolon(csv.excel):
  """Describe the usual properties of Excel-generated TAB-delimited files."""
  delimiter = ';'

csv.register_dialect("excel-semicolon", excel_semicolon)


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

  def __init__(self,l,f,sub_schema,attr_types,ldap_charset='utf-8',csv_charset='utf-8'):
    ldap.async.AsyncSearchHandler.__init__(self,l)
    self._csv_writer = csv.writer(f,dialect='excel-semicolon')
    self._s = sub_schema
    self._attr_types = attr_types
    self._ldap_charset = ldap_charset
    self._csv_charset = csv_charset

  def preProcessing(self):
    self._csv_writer.writerow(self._attr_types)

  def _processSingleResult(self,resultType,resultItem):
    if self._entryResultTypes.has_key(resultType):
      entry = ldaputil.schema.Entry(self._s,resultItem[0],resultItem[1])
      csv_row_list = []
      for attr_type in self._attr_types:
        csv_col_value_list = []
        for attr_value in entry.get(attr_type,['']):
          try:
            csv_col_value = attr_value.decode(self._ldap_charset).encode(self._csv_charset)
          except UnicodeError:
            csv_col_value = attr_value.encode('base64').replace('\r','').replace('\n','')
          csv_col_value_list.append(csv_col_value)
        csv_row_list.append('|'.join(csv_col_value_list))
      self._csv_writer.writerow(csv_row_list)

try:
  import pyExcelerator
except ImportError:
  ExcelWriter = None
else:

  pyExcelerator.UnicodeUtils.DEFAULT_ENCODING = 'cp1251'

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

    def __init__(self,l,f,sub_schema,attr_types,ldap_charset='utf-8'):
      ldap.async.AsyncSearchHandler.__init__(self,l)
      self._f = f
      self._s = sub_schema
      self._attr_types = attr_types
      self._ldap_charset = ldap_charset
      self._workbook = pyExcelerator.Workbook()
      self._worksheet = self._workbook.add_sheet('web2ldap_export')
      self._row_counter = 0

    def preProcessing(self):
      for col in range(len(self._attr_types)):
        self._worksheet.write(0,col,self._attr_types[col])
      self._row_counter += 1

    def postProcessing(self):
      self._workbook.save(self._f)

    def _processSingleResult(self,resultType,resultItem):
      if self._entryResultTypes.has_key(resultType):
        entry = ldaputil.schema.Entry(self._s,resultItem[0],resultItem[1])
        csv_row_list = []
        for attr_type in self._attr_types:
          csv_col_value_list = []
          for attr_value in entry.get(attr_type,['']):
            try:
              csv_col_value = attr_value.decode(self._ldap_charset)
            except UnicodeError:
              csv_col_value = attr_value.encode('base64').replace('\r','').replace('\n','').decode('ascii')
            csv_col_value_list.append(csv_col_value)
          csv_row_list.append('|'.join(csv_col_value_list))
        for col in range(len(csv_row_list)):
          self._worksheet.write(self._row_counter,col,csv_row_list[col])
        self._row_counter += 1


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,sub_schema,base64_attrs=None,dsml_comment=''):
    ldap.async.AsyncSearchHandler.__init__(self,l)
    self._dsmlWriter = dsml.DSMLWriter(f,base64_attrs or [],dsml_comment)
    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,connLDAPUrl):
  """
  Search for entries and output results as table, pretty-printable output
  or LDIF formatted
  """

  scope,filterstr = connLDAPUrl.scope,w2lapp.core.str2unicode(connLDAPUrl.filterstr,form.accept_charset)

  search_submit = form.getInputValue('search_submit',[u'Search'])[0]

  if search_submit!=u'Search':
    w2lapp.searchform.w2l_SearchForm(
      sid,outf,command,form,ls,dn,
      searchform_mode='adv',
      Msg='',
      filterstr=u'',
      scope=scope
    )
    return

  # This should speed up things
  utf2display = form.utf2display

  search_output = form.getInputValue('search_output',['table'])[0]
  search_opattrs = form.getInputValue('search_opattrs',['no'])[0]=='yes'
  search_root = form.getInputValue('search_root',[dn])[0]

  # Hmm, this retrieves sub schema sub entry for the search root.
  # Theoretically it could be different for all search results.
  # But what the hey...
  sub_schema = ls.retrieveSubSchema(search_root,w2lapp.cnf.GetParam(ls,'_schema',None))

  if scope==None:
    scope = ldap.SCOPE_SUBTREE

  search_filter = form.getInputValue('filterstr',[filterstr])

  if ('search_option' in form.inputFieldNames) and \
     ('search_attr' in form.inputFieldNames) and \
     ('search_string' in form.inputFieldNames):
    search_mode = form.getInputValue('search_mode',[ur'(&%s)'])[0]
    search_option = form.getInputValue('search_option',[])
    search_attr = form.getInputValue('search_attr',[])
    search_mr = form.getInputValue('search_mr',[None]*len(search_attr))
    search_string = form.getInputValue('search_string',[])
    if (len(search_option)==len(search_attr)==len(search_mr)==len(search_string)):
      # Get search mode
      # Build LDAP search filter from input data of advanced search form
      for i in range(len(search_attr)):
        if search_option[i]==w2lapp.searchform.SEARCH_OPT_EXISTS:
          search_filter.append(search_option.format(at=search_attr[i]))
        elif search_string[i]:
          if not '*' in search_option[i]:
            # If an exact assertion value is needed we can normalize via plugin class
            attr_instance = syntax_registry.attrInstance(
              None,form,ls,dn,sub_schema,search_attr[i],None,entry=None
            )
            search_string[i] = attr_instance.sanitizeInput(search_string[i])
          if search_mr[i]:
            search_mr_string = ':%s:' % (search_mr[i])
          else:
            search_mr_string = ''
          search_filter.append(search_option[i] % (
            ''.join((search_attr[i],search_mr_string)),
            escape_ldap_filter_chars(search_string[i])
          ))
    else:
      raise w2lapp.core.ErrorExit(u'Invalid search form data.')

  # Wipe out all nullable search_filter list items
  search_filter = filter(None,search_filter)

  if not search_filter:
    w2lapp.searchform.w2l_SearchForm(
      sid,outf,command,form,ls,dn,
      searchform_mode={True:'adv',False:'exp'}['search_attr' in form.inputFieldNames],
      Msg='Empty search values.',
      filterstr=u'',
      scope=scope
    )
    return
  elif len(search_filter)==1:
    filterstr = search_filter[0]
  elif len(search_filter)>1:
    filterstr = search_mode % (u''.join(search_filter))

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

  search_lastmod = int(form.getInputValue('search_lastmod',[-1])[0])
  if search_lastmod>0:
    timestamp_str = unicode(time.strftime('%Y%m%d%H%M%S',time.gmtime(time.time()-search_lastmod)),'ascii')
    if sub_schema.sed[ldap.schema.AttributeType].has_key('1.2.840.113556.1.2.2') and \
       sub_schema.sed[ldap.schema.AttributeType].has_key('1.2.840.113556.1.2.3'):
      # Assume we're searching MS Active Directory
      filterstr2 = u'(&(|(whenCreated>=%s.0Z)(whenChanged>=%s.0Z))%s)' % (
        timestamp_str,timestamp_str,filterstr
      )
    else:
      # Assume standard LDAPv3 attributes
      filterstr2 = u'(&(|(createTimestamp>=%sZ)(modifyTimestamp>=%sZ))%s)' % (
        timestamp_str,timestamp_str,filterstr
      )
  else:
    filterstr2 = filterstr

  requested_attrs = w2lapp.cnf.GetParam(ls,'requested_attrs',[])

  search_attrs = [
    a.strip().encode('ascii')
    for a in form.getInputValue('search_attrs',[u','.join(connLDAPUrl.attrs or [])])[0].split(u',')
    if a.strip()
  ]

  search_attr_set = ldaputil.schema.SchemaElementOIDSet(sub_schema,ldap.schema.models.AttributeType,search_attrs)
  search_attrs = search_attr_set.names()

  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.ErrorExit(u'No templates for printing 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 = set(read_attrs)
        read_attrs.update(GrabKeys(print_template_str_dict[oc]).keys)
    result_handler = ldap.async.List(ls.l)

  elif search_output in ['table','raw']:

    search_tdtemplate = ldap.cidict.cidict(w2lapp.cnf.GetParam(ls,'search_tdtemplate',{}))
    search_tdtemplate_keys = search_tdtemplate.keys()
    search_tdtemplate_keys_lower = search_tdtemplate.data.keys()
    search_tablistattrs = w2lapp.cnf.GetParam(ls,'search_tablistattrs',[])

    search_tdtemplate_attrs_lower = {}
    for oc in search_tdtemplate_keys_lower:
      search_tdtemplate_attrs_lower[oc] = [k.lower() for k in GrabKeys(search_tdtemplate[oc]).keys]

    # Build the list of attributes to read from templates
    if not search_output=='raw':
      read_attrs = set(['objectClass'])
      for oc in search_tdtemplate_keys:
        read_attrs.update(GrabKeys(search_tdtemplate[oc]).keys)
      if search_tablistattrs:
        read_attrs.update(search_tablistattrs)
    else:
      read_attrs = set([])
    read_attrs.update([
      'subschemaSubentry','displayName','description','structuralObjectClass',
      'hasSubordinates','subordinateCount',
      'numSubordinates',
      'numAllSubordinates', #  Siemens DirX
      'countImmSubordinates','countTotSubordinates', # Critical Path Directory Server
      'msDS-Approx-Immed-Subordinates' # MS Active Directory
    ])
    # Create async search handler instance
    result_handler = ldap.async.List(ls.l)

  elif search_output in ('ldif','ldif1'):
    # read all attributes
    read_attrs = search_attrs or ({0:['*'],1:['*','+']}[ls.supportsAllOpAttr and search_opattrs]+requested_attrs) or None
    result_handler = ldap.async.LDIFWriter(ls.l,outf)

  elif search_output in ('csv','excel'):

    read_attrs = [ a for a in search_attrs if not a in ('*','+') ]
    if not read_attrs:
      w2lapp.searchform.w2l_SearchForm(
        sid,outf,command,form,ls,dn,searchform_mode='exp',
        Msg='<strong>Attributes to be read have to be explicitly defined for table-structured data export!</strong>',
        filterstr=filterstr,
        scope=scope,
        search_root=search_root
      )
      return
    result_handler = {'csv':CSVWriter,'excel':ExcelWriter}[search_output](ls.l,outf,sub_schema,read_attrs)

  elif search_output=='dsml1':
    # read all attributes
    read_attrs = search_attrs or ({0:['*'],1:['*','+']}[ls.supportsAllOpAttr and search_opattrs]+requested_attrs) or None
    result_handler = DSMLWriter(ls.l,outf,sub_schema)

  search_ldap_url = ls.ldapUrl(dn=search_root)
  search_ldap_url.filterstr = filterstr2.encode(ls.charset)
  search_ldap_url.scope = scope
  search_ldap_url.attrs = search_attrs

  if search_resnumber:
    search_size_limit = search_resminindex+search_resnumber
  else:
    search_size_limit = -1

  try:
    # Start the search
    result_handler.startSearch(
      search_root.encode(ls.charset),
      scope,
      filterstr2.encode(ls.charset),
      attrList=[ a.encode(ls.charset) for a in read_attrs or [] ] or None,
      attrsOnly=0,
      sizelimit=search_size_limit
    )
  except (ldap.FILTER_ERROR,ldap.INAPPROPRIATE_MATCHING),e:
    # 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=' '.join((
        w2lapp.gui.LDAPError2ErrMsg(e,form,charset=ls.charset),
        form.utf2display(filterstr2),
      )),
      filterstr=filterstr,
      scope=scope
    )
    return
  except ldap.NO_SUCH_OBJECT,e:
    if dn:
      raise e

  if search_output in ['table','raw']:

    SearchWarningMsg = ''
    max_result_msg = ''
    num_all_search_results,num_all_search_continuations = None,None

    try:
      result_handler.processResults(
        search_resminindex,search_resnumber,timeout=ls.timeout
      )
    except (ldap.SIZELIMIT_EXCEEDED,ldap.ADMINLIMIT_EXCEEDED),e:
      if search_size_limit<0 or result_handler.endResultBreak<search_size_limit:
        SearchWarningMsg = w2lapp.gui.LDAPError2ErrMsg(e,form,ls.charset,template=SizeLimitMsg)
      partial_results = 1
      # Retrieve the overall number of search results by resending the
      # search request without size limit but with the SearchNoOpControl attached
      if SearchNoOpControl.controlType in ls.rootDSE.get('supportedControl',[]):
        try:
          num_all_search_results,num_all_search_continuations = ls.l.noop_search_st(
            search_root.encode(ls.charset),
            scope,
            filterstr=filterstr2.encode(ls.charset),
            timeout=SEARCH_NOOP_TIMEOUT,
          )
        except LDAPLimitErrors,e:
          pass
    except (ldap.FILTER_ERROR,ldap.INAPPROPRIATE_MATCHING),e:
      # 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=w2lapp.gui.LDAPError2ErrMsg(e,form,charset=ls.charset),
        filterstr=filterstr,
        scope=scope
      )
      return
    except (ldap.NO_SUCH_OBJECT,ldap.UNWILLING_TO_PERFORM),e:
      partial_results = 0
      if dn or scope!=ldap.SCOPE_ONELEVEL:
        raise e
    else:
      partial_results = 0

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

    # HACK! Searching the root level the namingContexts is
    # appended if not already received in search result
    if not dn and scope==ldap.SCOPE_ONELEVEL:
      d = ldap.cidict.cidict()
      for result_dn in ls.namingContexts:
        if result_dn:
          d[result_dn] = result_dn
      for r in result_dnlist:
        result_dn = r[1][0]
        if result_dn!=None and d.has_key(result_dn):
          del d[result_dn]
      result_dnlist.extend([
        (ldap.RES_SEARCH_ENTRY,(result_dn.encode(ls.charset),{}))
        for result_dn in d.values()
      ])

    result_dnlist.sort()

    ContextMenuList = []

    if result_dnlist:

      # There's still data left to be read
      if search_size_limit>0 or search_resminindex:

        prev_resminindex = max(0,search_resminindex-search_resnumber)

        if search_resminindex+search_resnumber<=resind:
          partial_results = 1
          ContextMenuList.append(
            form.applAnchor(
              'search','-&gt;&gt;',sid,
              [
                ('dn',dn),
                ('search_root',search_root),
                ('filterstr',filterstr),
                ('search_output',search_output),
                ('search_resminindex',str(search_resminindex+search_resnumber)),
                ('search_resnumber',str(search_resnumber)),
                ('search_lastmod',unicode(search_lastmod)),
                ('scope',str(scope)),
                ('search_attrs',u','.join(search_attrs)),
              ],
              title=u'Display next search results %d to %d' % (1+search_resminindex+search_resnumber,search_resminindex+2*search_resnumber),
            )
          )

        if search_resminindex:
          partial_results = 1
          ContextMenuList.append(
            form.applAnchor(
              'search','&lt;&lt;-',sid,
              [
                ('dn',dn),
                ('search_root',search_root),
                ('filterstr',filterstr),
                ('search_output',search_output),
                ('search_resminindex',str(prev_resminindex)),
                ('search_resnumber',str(search_resnumber)),
                ('search_lastmod',unicode(search_lastmod)),
                ('scope',str(scope)),
                ('search_attrs',u','.join(search_attrs)),
              ],
              title=u'Display previous search results %d to %d' % (1+prev_resminindex,prev_resminindex+search_resnumber),
            )
          )

        if partial_results:
          ContextMenuList.append(
            form.applAnchor(
              'search','Display all',sid,
              [
                ('dn',dn),
                ('search_root',search_root),
                ('filterstr',filterstr),
                ('search_output',search_output),
                ('search_resnumber',u'0'),
                ('search_lastmod',unicode(search_lastmod)),
                ('scope',str(scope)),
                ('search_attrs',u','.join(search_attrs)),
              ],
              title=u'Display all search results at once',
            )
          )
          if num_all_search_results!=None:
            max_result_msg = '(of %d) ' % (num_all_search_results)

        result_message = 'Results %d - %d %sof %s search below %s with filter &quot;%s&quot;.' % (
            search_resminindex+1,
            resind,
            max_result_msg,
            ldaputil.base.SEARCH_SCOPE_STR[scope],
            utf2display(search_root),
            utf2display(filterstr2),
        )

      else:
        result_message = '%d results for %s search below %s with filter &quot;%s&quot;.' % (
          len(result_dnlist),
          ldaputil.base.SEARCH_SCOPE_STR[scope],
          utf2display(search_root),
          utf2display(filterstr2),
        )

      ContextMenuList.extend([
        form.applAnchor(
          'searchform','Refine Filter',sid,
          [
            ('dn',dn),
            ('search_root',search_root),
            ('searchform_mode','exp'),
            ('filterstr',filterstr),
            ('search_attrs',u','.join(search_attrs)),
            ('search_lastmod',unicode(search_lastmod)),
            ('scope',str(scope)),
          ],
          title=u'Modify search parameters: filter, search root/scope, etc.',
        ),
        form.applAnchor(
          'search','Negate search',sid,
          [
            ('dn',dn),
            ('search_root',search_root),
            ('search_output',{0:'raw',1:'table'}[search_output=='raw']),
            ('scope',str(scope)),
            ('filterstr','(!%s)' % filterstr),
            ('search_resminindex',str(search_resminindex)),
            ('search_resnumber',str(search_resnumber)),
            ('search_lastmod',unicode(search_lastmod)),
            ('search_attrs',u','.join(search_attrs)),
          ],
          title=u'Search with negated search filter',
        ),
        form.applAnchor(
          'search',
          {0:'Raw',1:'Table'}[search_output=='raw'],
          sid,
          [
            ('dn',dn),
            ('search_root',search_root),
            ('search_output',{0:'raw',1:'table'}[search_output=='raw']),
            ('scope',str(scope)),
            ('filterstr',filterstr),
            ('search_resminindex',str(search_resminindex)),
            ('search_resnumber',str(search_resnumber)),
            ('search_lastmod',unicode(search_lastmod)),
            ('search_attrs',u','.join(search_attrs)),
          ],
          title=u'Display %s of search results' % (
            {0:u'distinguished names',1:u'attributes'}[search_output=='raw']
          ),
        ),
      ])

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

      export_field = w2lapp.form.ExportFormatSelect('search_output')
      export_field.charset = form.accept_charset

      outf.write('<div id="Message" class="Main">\n%s\n%s\n(%s)\n%s\n' % (
          SearchWarningMsg,
          result_message,
          search_ldap_url.htmlHREF(hrefText='LDAP URL',hrefTarget=None),
          form.formHTML(
            'search','Export',sid,'GET',
            [
              ('dn',dn),
              ('search_root',search_root),
              ('scope',unicode(scope)),
              ('filterstr',filterstr),
              ('search_lastmod',unicode(search_lastmod)),
              ('search_resnumber',u'0'),
              ('search_attrs',u','.join(search_attrs)),
            ],
            extrastr = export_field.inputHTML()+'Incl. op. attrs.:'+w2lapp.form.InclOpAttrsCheckbox('search_opattrs',u'Request operational attributes',default="yes",checked=0).inputHTML(),
            target='web2ldapexport',
          ),
        )
      )

      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:
        mailtolist = [ urllib.quote(m) for m in mailtolist ]
        outf.write('Mail to all <a href="mailto:%s?cc=%s">Cc:-ed</a> - <a href="mailto:?bcc=%s">Bcc:-ed</a>' % (
          mailtolist[0],
          ','.join(mailtolist[1:]),
          ','.join(mailtolist)
        ))

      outf.write('<table id="SrchResList">\n')

      for r in result_dnlist:

        if is_search_reference.has_key(r[0]):

          # Display a search continuation (search reference)
          entry = ldap.cidict.cidict({})
          try:
            refUrl = LDAPUrl(r[1][1][0])
          except ValueError,e:
            command_table = []
            result_dd_str='Search reference (NON-LDAP-URI) =&gt; %s' % (w2lapp.gui.utf2display(r[1][1][0]))
          else:
            result_dd_str='Search reference =&gt; %s' % (refUrl.htmlHREF(hrefTarget=None))
            if scope==ldap.SCOPE_SUBTREE:
              refUrl.scope = refUrl.scope or scope
              refUrl.filterstr = (unicode(refUrl.filterstr or '',ls.charset) or filterstr).encode(form.accept_charset)
              command_table = [
                form.applAnchor(
                  'search','Continue search',
                  {0:sid,1:None}[refUrl.initializeUrl()!=ls.uri],
                  [('ldapurl',refUrl.unparse())],
                  title=u'Follow this search continuation',
                )
              ]
            else:
              command_table = []
              refUrl.filterstr = filterstr
              refUrl.scope=ldap.SCOPE_BASE
              command_table.append(form.applAnchor(
                'read','Read',
                {0:sid,1:None}[refUrl.initializeUrl()!=ls.uri],
                [('ldapurl',refUrl.unparse())],
                title=u'Display single entry following search continuation',
              ))
              refUrl.scope=ldap.SCOPE_ONELEVEL
              command_table.append(form.applAnchor(
                'search','Down',
                {0:sid,1:None}[refUrl.initializeUrl()!=ls.uri],
                [('ldapurl',refUrl.unparse())],
                title=u'Descend into tree following search continuation',
              ))

        elif is_search_result.has_key(r[0]):

          # Display a search result with entry's data
          dn,entry = unicode(r[1][0],ls.charset),ldap.cidict.cidict(r[1][1])

          if search_output=='raw':

            # Output DN
            result_dd_str=utf2display(dn)

          else:

            objectclasses_lower_set = set([o.lower() for o in entry.get('objectClass',[])])
            tdtemplate_oc = objectclasses_lower_set.intersection(search_tdtemplate_keys_lower)

            if tdtemplate_oc:

              template_attrs = set([])
              for oc in tdtemplate_oc:
                template_attrs.update(search_tdtemplate_attrs_lower[oc])
              tableentry_attrs = template_attrs.intersection(entry.data.keys())
              if tableentry_attrs:
                # Output entry with the help of pre-defined templates
                tableentry = w2lapp.read.DisplayEntry(sid,form,ls,dn,sub_schema,entry,'searchSep',0)
                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=utf2display(dn)

            elif entry.has_key('displayName'):
              result_dd_str = utf2display(ls.uc_decode(entry['displayName'][0])[0])

            elif search_tablistattrs and entry.has_key(search_tablistattrs[0]):
              tdlist = []
              for attr_type in search_tablistattrs:
                if entry.has_key(attr_type):
                  tdlist.append(', '.join([
                    w2lapp.gui.DataStr(
                      sid,form,ls,dn,sub_schema,attr_type,value,commandbutton=0
                    )
                    for value in entry[attr_type]
                  ]))
              result_dd_str='<br>\n'.join(filter(None,tdlist))

            else:
              # Output DN
              result_dd_str=utf2display(dn)

          # Build the list for link table
          command_table = []

          # A [Read] link is added in any case
          read_title_list = [ dn ]
          for attr_type in (u'description',u'structuralObjectClass'):
            try:
              first_attr_value = unicode(entry[attr_type][0],ls.charset)
            except KeyError:
              pass
            else:
              read_title_list.append(u'%s: %s' % (attr_type,first_attr_value))
          command_table.append(
            form.applAnchor(
              'read','Read',sid,
              [('dn',dn)],
              title=u'\n'.join(read_title_list)
            )
          )

          # Try to determine from entry's attributes if there are subordinates
          hasSubordinates = entry.get('hasSubordinates',['TRUE'])[0].upper()=='TRUE'
          try:
            subordinateCountFlag = int(entry.get('subordinateCount',entry.get('numAllSubordinates',entry.get('msDS-Approx-Immed-Subordinates',['1'])))[0])
          except ValueError:
            subordinateCountFlag = 1

          # If subordinates or unsure a [Down] link is added
          if hasSubordinates and subordinateCountFlag>0:

            down_title_list = ['Browse subordinates']

            # Determine number of direct subordinates
            numSubOrdinates = entry.get('numSubOrdinates',entry.get('subordinateCount',entry.get('countImmSubordinates',entry.get('msDS-Approx-Immed-Subordinates',[None]))))[0]
            if numSubOrdinates!=None:
              numSubOrdinates = int(numSubOrdinates)
              down_title_list.append('direct: %d' % (numSubOrdinates))
            # Determine total number of subordinates
            numAllSubOrdinates = entry.get('numAllSubOrdinates',entry.get('countTotSubordinates',[None]))[0]
            if numAllSubOrdinates!=None:
              numAllSubOrdinates = int(numAllSubOrdinates)
              down_title_list.append(u'total: %d' % (numAllSubOrdinates))

            command_table.append(form.applAnchor(
                'search','Down',sid,
                (
                  ('dn',dn),
                  ('scope',w2lapp.searchform.SEARCH_SCOPE_STR_ONELEVEL),
                  ('filterstr',u'(objectClass=*)'),
                ),
                title=u'\r\n'.join(down_title_list),
            ))

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

        outf.write("""
        <tr>
          <td class="CommandTable">\n%s\n</td>
          <td class="SrchRes">\n%s\n</td>
        </tr>
        """ % (
            '\n'.join(command_table),
            result_dd_str
          )
        )

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

      w2lapp.gui.PrintFooter(outf,form)

    else:

      ##############################
      # Empty search results
      ##############################

      ContextMenuList = [
        form.applAnchor(
          'searchform','Refine Filter',sid,
          [
            ('dn',dn),
            ('searchform_mode','exp'),
            ('search_root',search_root),
            ('filterstr',filterstr),
            ('search_lastmod',unicode(search_lastmod)),
            ('search_attrs',','.join(read_attrs)),
            ('scope',str(scope)),
          ],
        )
      ]

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

  elif search_output=='print':

    try:
      result_handler.processResults(timeout=ls.timeout)
    except (ldap.SIZELIMIT_EXCEEDED,ldap.ADMINLIMIT_EXCEEDED),e:
      result_handler.postProcessing()

    result_handler.allResults.sort()

    table=[]
    for r in result_handler.allResults:
      if is_search_result.has_key(r[0]):
        entry = r[1][1]
        objectclasses = entry.get('objectclass',entry.get('objectClass',[]))
        template_oc = list(set([o.lower() for o in objectclasses]).intersection(
          [s.lower() for s in print_template_str_dict.keys()]
        ))
        if template_oc:
          tableentry = msbase.CaseinsensitiveStringKeyDict(default='')
          attr_list=entry.keys()
          for attr in attr_list:
            tableentry[attr] = ', '.join([
              utf2display(unicode(attr_value,ls.charset))
              for attr_value in entry[attr]
            ])
          table.append(print_template_str_dict[template_oc[0]] % (tableentry))

    # Output search results as pretty-printable table without buttons
    w2lapp.gui.PrintHeader(sid,outf,form,'Pretty-Printable Search Results',w2lapp.cnf.GetParam(ls,'link_css',''))
    outf.write("""<h1>%s</h1>
<table
  class="PrintSearchResults"
  rules="rows"
  id="PrintTable"
  summary="Table with search results formatted for printing">
    <colgroup span="%d">
    """ % (utf2display(dn),print_cols))
    tdwidth=int(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 CSV
  ################################################################

  elif search_output=='csv':

    # Output search results as LDIF
    pyweblib.httphelper.SendHeader(
      outf,'text/csv',charset='UTF-8',
      additional_header={
        'Cache-Control':'private,no-store,no-cache,max-age=0,must-revalidate',
        'Content-Disposition':'inline; filename=web2ldap-export.csv',
      }
    )
    try:
      result_handler.processResults(timeout=ls.timeout)
    except (ldap.SIZELIMIT_EXCEEDED,ldap.ADMINLIMIT_EXCEEDED),e:
      result_handler.postProcessing()

  ################################################################
  # Output Excel workbook
  ################################################################

  elif search_output=='excel':

    # Output search results as LDIF
    pyweblib.httphelper.SendHeader(
      outf,'application/vnd.ms-excel',charset='UTF-8',
      additional_header={
        'Cache-Control':'private,no-store,no-cache,max-age=0,must-revalidate',
        'Content-Disposition':'inline; filename=web2ldap-export.xls',
      }
    )
    try:
      result_handler.processResults(timeout=ls.timeout)
    except (ldap.SIZELIMIT_EXCEEDED,ldap.ADMINLIMIT_EXCEEDED),e:
      result_handler.postProcessing()


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

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

    # Output search results as LDIF
    pyweblib.httphelper.SendHeader(
      outf,'text/plain',charset='UTF-8',
      additional_header={
        'Cache-Control':'private,no-store,no-cache,max-age=0,must-revalidate',
        'Content-Disposition':'inline; filename=web2ldap-export.ldif',
      }
    )
    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)
    )

    try:
      result_handler.processResults(timeout=ls.timeout)
    except (ldap.SIZELIMIT_EXCEEDED,ldap.ADMINLIMIT_EXCEEDED),e:
      result_handler.postProcessing()


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

  elif search_output=='dsml1':

    # Output search results as DSML in a rather primitive way.
    # (level 1 producer)
    pyweblib.httphelper.SendHeader(
      outf,'text/xml',charset='UTF-8',
      additional_header={
        'Cache-Control':'private',
        'Content-Disposition':'inline; filename=web2ldap-export.dsml'
      }
    )
    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),
    ])
    try:
      result_handler.processResults(timeout=ls.timeout)
    except (ldap.SIZELIMIT_EXCEEDED,ldap.ADMINLIMIT_EXCEEDED),e:
      result_handler.postProcessing()
