# -*- coding: utf-8 -*-
"""
w2lapp.modify.py: modify an entry

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 ldap,ldif,ldap.schema, \
       ldapsession,w2lapp.core,w2lapp.cnf,w2lapp.gui,w2lapp.addmodifyform,w2lapp.add

from w2lapp.schema.syntaxes import syntax_registry,LDAPSyntaxValueError

try:
  from cStringIO import StringIO
except ImportError:
  from StringIO import StringIO

from ldap.schema.models import AttributeType
from ldaputil.modlist2 import modifyModlist

def GetEntryfromInputForm(form,ls,dn,sub_schema):

  invalid_attrs = []

  e = w2lapp.addmodifyform.UserEditableEntry(ls,sub_schema,dn.encode(ls.charset),{})
  try:
    in_ldif = form.field['in_ldif'].getLDIFRecords()
  except ValueError,e:
    raise w2lapp.core.ErrorExit(u'LDIF parsing error: %s' % (str(e)))
  else:
    if in_ldif:
      e.update(in_ldif[0][1])

  # Get all the attribute types
  in_attrtype_list = [
    a.encode('ascii')
    for a in form.getInputValue('in_attrtype',[])
  ]
  in_binattrs = [
    a.encode('ascii')
    for a in form.getInputValue('in_binattrtype',[])
  ]
  in_attrtype_list.extend(in_binattrs)
  # Grab the Unicode input strings and convert to LDAP charset
  in_value_list = [
    a.encode(ls.charset)
    for a in form.getInputValue('in_value',[])
  ]
  # Grab the file-based input and leave as is
  in_value_list.extend([
    a for a in form.getInputValue('in_binvalue',[])
  ])

  if len(in_attrtype_list)!=len(in_value_list):
    raise w2lapp.core.ErrorExit(u'Different count of attribute types and values.')

  for i in range(len(in_attrtype_list)):
    attr_type = in_attrtype_list[i]
    attr_instance = syntax_registry.attrInstance(None,form,ls,dn,sub_schema,attr_type,None,entry=e)
    try:
      attr_value = attr_instance.sanitizeInput(in_value_list[i])
      attr_instance.validate(attr_value)
    except LDAPSyntaxValueError:
      invalid_attrs.append(attr_type)
    if attr_value:
      if not attr_type in e:
        e[attr_type] = []
      e[attr_type].append(attr_value)
  return e,invalid_attrs,in_binattrs # GetEntryfromInputForm()


def ModlistLDIF(dn,modlist):
  """
  Return a string containing a HTML table showing modifiy operations
  and attr type/value pairs
  """
  s = []
  s.append('<pre summary="Modify list">')
  f = StringIO()
  ldif_writer = ldif.LDIFWriter(f)
  ldif_writer.unparse(dn.encode('utf-8'),modlist)
  s.append(f.getvalue())
  s.append('</pre>')
  return '\n'.join(s) # ModlistTable()


##############################################################################
# Modify existing entry
##############################################################################

def w2l_Modify(sid,outf,command,form,ls,dn):

  # Determine whether Relax Rules control is in effect
  relax_rules_enabled = ls.l._get_server_ctrls('**write**').has_key(ldapsession.CONTROL_RELAXRULES)

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

  in_wrtattroids = form.getInputValue('in_wrtattroids',[])
  if in_wrtattroids==['nonePseudoValue;x-web2ldap-None']:
    writeable_attr_oids = None
  else:
    writeable_attr_oids = set([ a.encode(ls.charset) for a in in_wrtattroids ])

  new_entry,invalid_attrs,in_binattrs = GetEntryfromInputForm(form,ls,dn,sub_schema)

  # Check if the user just switched input type form
  if 'input_formtype' in form.inputFieldNames:
    input_addattrtypes = form.getInputValue('input_addattrtype',[])
    for input_addattrtype in input_addattrtypes:
      if input_addattrtype:
        new_entry[input_addattrtype.encode('ascii')] = new_entry.get(input_addattrtype,[])+['']
    w2lapp.addmodifyform.w2l_ModifyForm(
      sid,outf,'modifyform',form,ls,dn,
      Msg='',
      entry=new_entry,
      skip_oc_input=1,
      writeable_attr_oids=writeable_attr_oids,
    )
    return

  if invalid_attrs:
    w2lapp.addmodifyform.w2l_ModifyForm(
      sid,outf,'modifyform',form,ls,dn,
      Msg='Syntax check failed for the following attributes: %s' % (
        ', '.join([ form.utf2display(unicode(v)) for v in invalid_attrs ])
      ),
      entry=new_entry,
      skip_oc_input=1,
      writeable_attr_oids=writeable_attr_oids,
    )
    return

  in_oldattrtypes = {}
  for a in form.getInputValue('in_oldattrtypes',[]):
    attr_type = a.encode('ascii')
    in_oldattrtypes[attr_type] = None

  in_assertion = form.getInputValue('in_assertion',[u'(objectClass=*)'])[0]

  try:
    old_entry,dummy = w2lapp.addmodifyform.ReadOldEntry(command,ls,dn,sub_schema,in_assertion,None)
  except ldap.NO_SUCH_OBJECT:
    raise w2lapp.core.ErrorExit(u'Old entry was removed or modified in between! You have to edit it again.')

  # Set up a dictionary of all attribute types to be ignored
  ignore_attr_types = ldap.cidict.cidict(
    {}.fromkeys([
      sub_schema.getoid(AttributeType,at_name,raise_keyerror=0)
      for at_name in w2lapp.add.ADD_IGNORE_ATTR_TYPES
    ])
  )

  if not relax_rules_enabled:
    # Add all attributes which have NO-USER-MODIFICATION set
    ignore_attr_types.update(sub_schema.no_user_mod_attr_oids)
    # Ignore attributes which are assumed to be constant (some operational attributes)
    ignore_attr_types.update(w2lapp.addmodifyform.ConfiguredConstantAttributes(ls))

  # All attributes currently read which were not viewable before
  # must be ignored to avoid problems with different access rights
  # after re-login
  ignore_attr_types.update({}.fromkeys([
    a
    for a in old_entry.keys()
    if not in_oldattrtypes.has_key(a)
  ]))

  # Ignore binary attributes from old entry data in any case
  for attr_type in old_entry.keys():
    syntax_class = syntax_registry.syntaxClass(sub_schema,attr_type)
    if not syntax_class.editable:
      ignore_attr_types[sub_schema.getoid(AttributeType,attr_type,raise_keyerror=0)] = None

  # Ignore binary attributes in new form input in any case
  ignore_attr_types.update({}.fromkeys([
    sub_schema.getoid(AttributeType,at_name,raise_keyerror=0)
    for at_name in in_binattrs
  ]))

  try:
    del ignore_attr_types['2.5.4.0']
  except KeyError:
    pass

  # Create modlist containing deltas
  modlist = modifyModlist(
    sub_schema,
    old_entry,new_entry,
    ignore_attr_types=ignore_attr_types.keys(),
    ignore_oldexistent=0,
  )
  # Binary values are always replaced
  for attr_type in new_entry.keys():
    syntax_class = syntax_registry.syntaxClass(sub_schema,attr_type)
    if (not syntax_class.editable) and \
       new_entry[attr_type] and \
       (not attr_type in old_entry or new_entry[attr_type]!=old_entry[attr_type]):
      modlist.append((ldap.MOD_REPLACE,attr_type,new_entry[attr_type]))

  if modlist:
    # Send modify-list to host
    try:
      ls.modifyEntry(dn,modlist,assertion_filter=in_assertion)
    except ldap.ASSERTION_FAILED:
      raise w2lapp.core.ErrorExit(u'Old entry was removed or modified in between! You have to edit it again.')
    except (
      ldap.CONSTRAINT_VIOLATION,
      ldap.INVALID_SYNTAX,
      ldap.OBJECT_CLASS_VIOLATION,
      ldap.OTHER,
      ldap.TYPE_OR_VALUE_EXISTS,
      ldap.UNDEFINED_TYPE,
    ),e:
      w2lapp.addmodifyform.w2l_ModifyForm(
        sid,outf,'modifyform',form,ls,dn,
        Msg=w2lapp.gui.LDAPError2ErrMsg(e,form,ls.charset),
        entry=new_entry,
        skip_oc_input=1
      )
      return
    else:
      # delete all cache entries referencing to old DN
      UserMsg = 'Modified entry %s with your input data:</p>\n%s' % (
        w2lapp.gui.DisplayDN(sid,form,ls,dn),
        ModlistLDIF(dn,modlist).encode(form.accept_charset)
      )
  else:
    UserMsg = 'No attributes modified of entry %s.' % (w2lapp.gui.DisplayDN(sid,form,ls,dn))

  # Output comes here
  w2lapp.gui.SimpleMessage(
    sid,outf,form,ls,dn,
    'Modify result',
    UserMsg,
    main_menu_list=w2lapp.gui.MainMenu(sid,form,ls,dn),
    context_menu_list=w2lapp.gui.ContextMenuSingleEntry(sid,form,ls,dn)
  )

