# -*- coding: utf-8 -*-
"""
w2lapp.delete.py: delete one entry or several entries

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,pyweblib.forms,ldap,ldap.async,ldapsession,ldaputil.base, \
       w2lapp.core,w2lapp.cnf,w2lapp.gui,w2lapp.ldapparams

# OID description dictionary from configuration directory
from ldapoidreg import oid as oid_desc_reg

class DeleteLeafs(ldap.async.AsyncSearchHandler):
  """
  Class for deleting entries which are results of a search.

  DNs of Non-leaf entries are collected in DeleteLeafs.nonLeafEntries.
  """
  _entryResultTypes = ldap.async._entryResultTypes

  def __init__(self,l,treeDeleteControl,delete_server_ctrls):
    ldap.async.AsyncSearchHandler.__init__(self,l)
    self.nonLeafEntries = []
    self.serverctrls = delete_server_ctrls
    self.treeDeleteControl = treeDeleteControl

  def startSearch(self,searchRoot,searchScope,filterStr='(objectClass=*)'):
    if not searchScope in [ldap.SCOPE_ONELEVEL,ldap.SCOPE_SUBTREE]:
      raise ValueError, "Parameter searchScope must be either ldap.SCOPE_ONELEVEL or ldap.SCOPE_SUBTREE."
    self.nonLeafEntries = []
    self.deletedEntries = 0
    self.noSuchObjectCounter = 0
    ldap.async.AsyncSearchHandler.startSearch(
      self,
      searchRoot,
      searchScope,
      filterStr=filterStr,
      attrList=[
        'hasSubordinates','subordinateCount','numSubordinates','numAllSubordinates',
        'msDS-Approx-Immed-Subordinates'],
      attrsOnly=0,
    )

  def _processSingleResult(self,resultType,resultItem):
    if self._entryResultTypes.has_key(resultType):
      # Don't process search references
      dn,entry = resultItem[0],ldap.cidict.cidict(resultItem[1])
      try:
        hasSubordinates = entry['hasSubordinates'][0].upper()=='TRUE'
      except KeyError:
        # hasSubordinates not available => look at numeric subordinate counters
        hasSubordinates = None
        try:
          subordinateCount = int(
            entry.get('subordinateCount',
              entry.get('numSubordinates',
                entry.get('numAllSubordinates',
                  entry['msDS-Approx-Immed-Subordinates']
              )))[0])
        except KeyError:
          subordinateCount = None
      else:
        subordinateCount = None
      if not self.treeDeleteControl and (hasSubordinates or (subordinateCount or 0)>0):
        self.nonLeafEntries.append(dn)
      else:
        try:
          self._l.delete_ext_s(dn,serverctrls=self.serverctrls)
        except ldap.NO_SUCH_OBJECT:
          # Don't do anything if the entry is already gone except counting
          # these sub-optimal cases
          self.noSuchObjectCounter = self.noSuchObjectCounter+1
        except ldap.NOT_ALLOWED_ON_NONLEAF:
          if hasSubordinates is None and subordinateCount is None:
            self.nonLeafEntries.append(dn)
          # Next statements are kind of a safety net and should never be executed
          elif not hasSubordinates or subordinateCount==0:
            raise ValueError,"Entry %s is non-leaf but is announced as leaf! hasSubordinates: %s, subordinateCount: %s" % (
              repr(dn),repr(hasSubordinates),repr(subordinateCount)
            )
          else:
            raise ValueError,"Entry %s contains invalid subordinate value! hasSubordinates: %s, subordinateCount: %s" % (
              repr(dn),repr(hasSubordinates),repr(subordinateCount)
            )
        else:
          # The entry was correctly deleted
          self.deletedEntries = self.deletedEntries+1


def DelTree(outf,ls,dn,scope,tree_delete_control,delete_server_ctrls):
  """
  Recursively delete entries below or including entry with name dn.
  """
  if scope==ldap.SCOPE_SUBTREE and tree_delete_control:
    # Try to directly delete the whole subtree with the tree delete control
    ls.l.delete_ext_s(dn,serverctrls=delete_server_ctrls)
    return True
  else:
    leafs_deleter = DeleteLeafs(ls.l,tree_delete_control,delete_server_ctrls)
    deleted_entries = 0
    non_leaf_entries = []
    not_deletable_entries = []
    while 1:
      # Send something for keeping the connection to the user's web browser open
      outf.write('');outf.flush()
      try:
        leafs_deleter.startSearch(dn,scope)
        leafs_deleter.processResults(timeout=ls.timeout)
      except ldap.NO_SUCH_OBJECT:
        break
      except (ldap.SIZELIMIT_EXCEEDED,ldap.ADMINLIMIT_EXCEEDED):
        deleted_entries += leafs_deleter.deletedEntries
        non_leaf_entries.extend(leafs_deleter.nonLeafEntries)
      else:
        deleted_entries += leafs_deleter.deletedEntries
        non_leaf_entries.extend(leafs_deleter.nonLeafEntries)
        break
    while non_leaf_entries:
      dn = non_leaf_entries.pop()
      try:
        leafs_deleter.startSearch(dn,ldap.SCOPE_SUBTREE)
        leafs_deleter.processResults(timeout=ls.timeout)
      except (ldap.SIZELIMIT_EXCEEDED,ldap.ADMINLIMIT_EXCEEDED):
        deleted_entries += leafs_deleter.deletedEntries
        non_leaf_entries.append(dn)
        non_leaf_entries.extend(leafs_deleter.nonLeafEntries)
      else:
        deleted_entries += leafs_deleter.deletedEntries
        if leafs_deleter.deletedEntries==0:
          not_deletable_entries.append(dn)
          continue
        non_leaf_entries.extend(leafs_deleter.nonLeafEntries)
    return deleted_entries # DelTree()


def w2l_Delete(sid,outf,command,form,ls,dn,connLDAPUrl):

  sub_schema = ls.retrieveSubSchema(dn,w2lapp.cnf.GetParam(ls,'_schema',None))

  delete_confirm = form.getInputValue('delete_confirm',[None])[0]

  delete_attr = form.getInputValue('delete_attr',[a.decode('ascii') for a in connLDAPUrl.attrs or []])
  delete_attr.sort()
  if delete_attr:
    scope = ldap.SCOPE_BASE
  else:
    scope = int(form.getInputValue('scope',[str(connLDAPUrl.scope or ldap.SCOPE_BASE)])[0])

  delete_scope_field = pyweblib.forms.Select(
    'scope',u'Scope of delete operation',1,
    options=(
      (str(ldap.SCOPE_BASE),'Only this entry'),
      (str(ldap.SCOPE_ONELEVEL),'All entries below this entry (recursive)'),
      (str(ldap.SCOPE_SUBTREE),'All entries including this entry (recursive)'),
    ),
    default=str(scope),
  )

  # Generate a list of requested LDAPv3 extended controls to be sent along
  # with a modify or delete request
  delete_ctrl_oids = form.getInputValue('delete_ctrl',[])
  delete_ctrl_tree_delete = ldapsession.CONTROL_TREEDELETE in delete_ctrl_oids

  if delete_confirm:

    if ls.l.protocol_version>=ldap.VERSION3:
      conn_server_ctrls = set([
        server_ctrl.controlType
        for server_ctrl in ls.l._serverctrls['**all**']+ls.l._serverctrls['**write**']+ls.l._serverctrls['delete_ext']
      ])
      delete_server_ctrls = [
        ldap.controls.LDAPControl(ctrl_oid,1,None)
        for ctrl_oid in delete_ctrl_oids
        if not ctrl_oid in conn_server_ctrls
      ] or None
    else:
      delete_server_ctrls = None

    if delete_confirm=='yes':

      # Recursive delete of whole sub-tree

      if scope in [ldap.SCOPE_ONELEVEL,ldap.SCOPE_SUBTREE]:

        ##########################################################
        # Recursive delete of entries in sub-tree
        ##########################################################

        begin_time_stamp = time.time()
        deleted_entries_count = DelTree(outf,ls,dn.encode(ls.charset),scope,delete_ctrl_tree_delete,delete_server_ctrls)
        end_time_stamp = time.time()

        old_dn = dn
        if scope==ldap.SCOPE_SUBTREE:
          dn = ldaputil.base.ParentDN(dn)
          ls.setDN(dn)
        w2lapp.gui.SimpleMessage(
          sid,outf,form,ls,dn,
          'Deleted sub-tree',
          'Deleted %d entries %s %s in %0.2f seconds.' % (
            deleted_entries_count,
            {
              1:'below',
              2:'below and including',
            }[scope],
            w2lapp.gui.DisplayDN(sid,form,ls,old_dn),
            end_time_stamp-begin_time_stamp,
          ),
          main_menu_list=w2lapp.gui.MainMenu(sid,form,ls,dn),
          context_menu_list=w2lapp.gui.ContextMenuSingleEntry(sid,form,ls,dn)
        )

      elif scope==ldap.SCOPE_BASE and delete_attr:

        ##########################################################
        # Delete attribute(s) from an entry with modify request
        ##########################################################

        mod_list = [
          (ldap.MOD_DELETE,attr_type,None)
          for attr_type in delete_attr
        ]
        ls.modifyEntry(dn,mod_list,serverctrls=delete_server_ctrls)
        w2lapp.gui.SimpleMessage(
          sid,outf,form,ls,dn,
          'Deleted Attribute(s)',
          """Deleted attribute(s) from entry %s:
          <ul>
            <li>
            %s
            </li>
          </ul>
          """ % (
            w2lapp.gui.DisplayDN(sid,form,ls,dn),
            '</li>\n<li>'.join([
              form.hiddenFieldHTML('delete_attr',attr_type,attr_type)
              for attr_type in delete_attr
            ]),
          ),
          main_menu_list=w2lapp.gui.MainMenu(sid,form,ls,dn),
          context_menu_list=w2lapp.gui.ContextMenuSingleEntry(sid,form,ls,dn)
        )

      elif scope==ldap.SCOPE_BASE:

        ##########################################################
        # Delete a single whole entry
        ##########################################################

        ls.deleteEntry(dn)
        old_dn = dn
        dn = ldaputil.base.ParentDN(dn)
        ls.setDN(dn)
        w2lapp.gui.SimpleMessage(
          sid,outf,form,ls,dn,
          'Deleted Entry',
          'Deleted entry %s.' % (
            w2lapp.gui.DisplayDN(sid,form,ls,old_dn)
          ),
          main_menu_list=w2lapp.gui.MainMenu(sid,form,ls,dn),
          context_menu_list=w2lapp.gui.ContextMenuSingleEntry(sid,form,ls,dn)
        )

    else:
      raise w2lapp.core.ErrorExit(u'Canceled delete.')

  else:

    ##########################################################
    # Show delete confirmation and delete mode select form
    ##########################################################

    dn_html = w2lapp.gui.DisplayDN(sid,form,ls,dn)

    # Read the editable attribute values of entry
    try:
      ldap_entry = ls.readEntry(
        dn,
        [a.encode(ls.charset) for a in delete_attr],
        search_filter='(objectClass=*)',
        no_cache=1,
        server_ctrls=None,
      )[0][1]
    except IndexError:
      ldap_entry = {}

    entry = ldaputil.schema.Entry(sub_schema,dn,ldap_entry)

    delete_ctrl_options=[
      (ctrl_oid,oid_desc_reg.get(ctrl_oid,(ctrl_oid,))[0])
      for ctrl_oid,ctrl_methods in w2lapp.ldapparams.AVAILABLE_BOOLEAN_CONTROLS.items()
      if '**write**' in ctrl_methods or 'modify_ext' in ctrl_methods
    ]
    delete_ctrl_field = pyweblib.forms.Select(
      'delete_ctrl',
      u'Extended controls',
      len(delete_ctrl_options),
      options=delete_ctrl_options,
      default=delete_ctrl_oids,
      size=3,
      multiSelect=1,
    )

    if delete_attr:
      scope_input_html = """
      <p>Delete following attribute(s) of entry %s?</p>
      <p>%s</p>
      <p>LDAPv3 extended controls to be used:</p>
      %s
      """ % (
        dn_html,
        '\n'.join([
          '<input type="checkbox" name="delete_attr" value="%s"%s>%s<br>' % (
            form.utf2display(attr_type,sp_entity='  '),
            ' checked'*(attr_type in entry),
            form.utf2display(attr_type),
          )
          for attr_type in delete_attr
        ]),
        delete_ctrl_field.inputHTML(),
      )

    else:

      hasSubordinates,numSubordinates,numAllSubordinates = ls.subOrdinates(dn)

      if hasSubordinates:

        if numSubordinates:
          numSubordinates_html = '<p>Number of direct subordinates: %d</p>' % (numSubordinates)
        else:
          numSubordinates_html = ''
        if numAllSubordinates:
          numAllSubordinates_html = '<p>Total number of subordinates: %d</p>' % (numAllSubordinates)
        else:
          numAllSubordinates_html = ''

        delete_ctrl_options.extend([
          (ctrl_oid,oid_desc_reg.get(ctrl_oid,(ctrl_oid,))[0])
          for ctrl_oid,ctrl_methods in w2lapp.ldapparams.AVAILABLE_BOOLEAN_CONTROLS.items()
          if '**write**' in ctrl_methods or 'delete_ext' in ctrl_methods
        ])
        delete_ctrl_field = pyweblib.forms.Select(
          'delete_ctrl',
          u'Extended controls',
          len(delete_ctrl_options),
          options=delete_ctrl_options,
          default=delete_ctrl_oids,
          size=3,
          multiSelect=1,
        )

        scope_input_html = """
          <p class="WarningMessage">
            Delete entry {text_dn}.<br>
            {text_num_sub_ordinates}
            {text_num_all_sub_ordinates}
          </p>
          <table>
            <tr><td>Delete mode:</td><td>{field_delete_scope}</td></tr>
            <tr><td>Use tree delete control:</td>
              <td>
                <input type="checkbox"
                       name="delete_ctrl"
                       value="{value_delete_ctrl_oid}"{value_delete_ctrl_checked}>
              </td>
            </tr>
            <tr><td>Additional controls:</td><td>{field_delete_ctrl}</td></tr>
          </table>
          <p><strong>
              Use recursive delete with extreme care!
              Might take some time.
          </strong></p>
        """.format(
          text_dn=dn_html,
          text_num_sub_ordinates=numSubordinates_html,
          text_num_all_sub_ordinates=numAllSubordinates_html,
          field_delete_scope=delete_scope_field.inputHTML(),
          value_delete_ctrl_oid=ldapsession.CONTROL_TREEDELETE,
          value_delete_ctrl_checked=' checked'*int(
            ldapsession.CONTROL_TREEDELETE in ls.supportedControl and \
            not 'OpenLDAProotDSE' in ls.rootDSE.get('objectClass',[])
          ),
          field_delete_ctrl=delete_ctrl_field.inputHTML(),
        )
      else:
        scope_input_html = """
          <p class="WarningMessage">Delete whole entry {text_dn}.</p>
        """.format(
          text_dn=dn_html,
        )

    # Output confirmation form
    w2lapp.gui.TopSection(
      sid,outf,form,ls,dn,
      'Delete entry?',
      w2lapp.gui.MainMenu(sid,form,ls,dn),
      context_menu_list=w2lapp.gui.ContextMenuSingleEntry(sid,form,ls,dn)
    )

    outf.write("""<div id="Message" class="Main">
{form_begin}
  {text_scope_input}
  <p><strong>Are you sure?</strong></p>
  {field_hidden_dn}
  <input type="submit" name="delete_confirm" value="yes">
  <input type="submit" name="delete_confirm" value="no">
</form>
</div>
""".format(
      form_begin=form.beginFormHTML('delete',sid,'POST'),
      text_scope_input=scope_input_html,
      field_hidden_dn=form.hiddenFieldHTML('dn',dn,u''),
    ))
    w2lapp.gui.PrintFooter(outf,form)

