# -*- coding: utf-8 -*-
"""
w2lapp.addmodifyform.py: input form for adding and modifying 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 re,msbase,pyweblib,ldap,ldif,ldap.schema,\
       ldaputil.schema,ldapsession,w2lapp.core,w2lapp.cnf,w2lapp.form,w2lapp.gui,w2lapp.read,w2lapp.schema

from w2lapp.schema.viewer import displayNameOrOIDList
from w2lapp.schema.syntaxes import syntax_registry
from msbase import GrabKeys

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


heading_msg = {'modifyform':'Modify entry','addform':'Add new entry'}

attrtype_pattern = r'[\w;.]+(;[\w_-]+)*'
attrvalue_pattern = r'(([^,]|\\,)*|".*?")'
rdn_pattern = attrtype_pattern + r'[ ]*=[ ]*' + attrvalue_pattern
dn_pattern = rdn_pattern + r'([ ]*,[ ]*' + rdn_pattern + r')*[ ]*'
ldif.dn_regex   = re.compile('^%s$' % dn_pattern)


class UserEditableEntry(ldaputil.schema.Entry):
  """
  Base class for a schema-aware entry containing
  solely attributes editable by a user
  """

  def __init__(self,ls,sub_schema,dn,ldap_entry):
    self._ls = ls
    ldaputil.schema.Entry.__init__(self,sub_schema,dn,ldap_entry)

  def get_object_class_oids(self):
    return dict([
      (self._s.name2oid[ldap.schema.models.ObjectClass].get(o,o),None)
      for o in self.get('objectClass',[])
    ])

  def attribute_types(self):
    # Initialize a list of assertions for filtering attribute types
    # displayed in the input form
    attr_type_filter = [
      ('no_user_mod',[0]),
#        ('usage',range(2)),
      ('collective',[0]),
    ]
    # Check whether Manage DIT control is in effect,
    # let python-ldap filter out OBSOLETE attribute types otherwise
    relax_rules_enabled = self._ls.l._get_server_ctrls('**write**').has_key(ldapsession.CONTROL_RELAXRULES)
    if not relax_rules_enabled:
      attr_type_filter.append(('obsolete',[0]))

    # Filter out extensibleObject
    object_class_oids = self.get_object_class_oids()
    try:
      del object_class_oids['1.3.6.1.4.1.1466.101.120.111']
    except KeyError:
      try:
        del object_class_oids['extensibleObject']
      except KeyError:
        pass

    required_attrs_dict,allowed_attrs_dict = self._s.attribute_types(
      object_class_oids.keys(),
      attr_type_filter=attr_type_filter,
      raise_keyerror=0,
      ignore_dit_content_rule=relax_rules_enabled
    )

    # Additional check whether to explicitly add object class attribute.
    # This is a work-around for LDAP servers which mark the
    # objectClass attribute as not modifiable (e.g. MS Active Directory)
    if not required_attrs_dict.has_key('2.5.4.0') and \
       not allowed_attrs_dict.has_key('2.5.4.0'):
      required_attrs_dict['2.5.4.0'] = self._s.get_obj(ldap.schema.ObjectClass,'2.5.4.0')
    return required_attrs_dict,allowed_attrs_dict


class InputFormEntry(ldaputil.schema.Entry):

  def __init__(self,sid,form,ls,dn,schema,entry,writeable_attr_oids,existing_object_classes=None):
    assert type(dn)==type(u'')
    ldaputil.schema.Entry.__init__(self,schema,dn.encode(ls.charset),entry)
    self.sid = sid
    self.form = form
    self.ls = ls
    self.entry = entry
    self.existing_object_classes = existing_object_classes
    self.set_dn(dn)
    self.relax_rules_enabled = self.ls.l._get_server_ctrls('**write**').has_key(ldapsession.CONTROL_RELAXRULES)
    self.manage_dsait_enabled = self.ls.l._get_server_ctrls('**all**').has_key(ldapsession.CONTROL_MANAGEDSAIT)
    self.writeable_attr_oids = writeable_attr_oids
    new_object_classes = set([
      self._s.getoid(ldap.schema.ObjectClass,oc_name)
      for oc_name in self.entry.get('objectClass',[])
    ]) - set([
      self._s.getoid(ldap.schema.ObjectClass,oc_name)
      for oc_name in existing_object_classes or []
    ])
    new_attribute_types = self._s.attribute_types(
      new_object_classes,
      raise_keyerror=0,
      ignore_dit_content_rule=self.relax_rules_enabled
    )
    old_attribute_types = self._s.attribute_types(
      existing_object_classes or [],
      raise_keyerror=0,
      ignore_dit_content_rule=self.relax_rules_enabled
    )
    self.new_attribute_types_oids = set()
    self.new_attribute_types_oids.update(new_attribute_types[0].keys())
    self.new_attribute_types_oids.update(new_attribute_types[1].keys())
    for at_oid in old_attribute_types[0].keys()+old_attribute_types[1].keys():
      try:
        self.new_attribute_types_oids.remove(at_oid)
      except KeyError:
        pass

  def set_dn(self,dn):
    self.dn = dn
    self.rdn_dict = self._get_rdn_dict(dn)

  def _get_rdn_dict(self,dn):
    assert type(dn)==type(u'')
    entry_rdn_dict = ldap.schema.Entry(self._s,None,ldaputil.base.rdn_dict(dn))
    for attr_type,attr_values in entry_rdn_dict.items():
      del entry_rdn_dict[attr_type]
      d = ldap.cidict.cidict()
      for attr_value in attr_values:
        attr_value = attr_value.encode(self.ls.charset)
        assert type(attr_value)==type('')
        d[attr_value] = None
      entry_rdn_dict[attr_type] = d
    return entry_rdn_dict

  def __getitem__(self,nameoroid):
    """
    Return HTML input field(s) for the attribute specified by nameoroid.
    """

    oid = self._at2key(nameoroid)[0]
    nameoroid_se = self._s.get_obj(ldap.schema.AttributeType,nameoroid)
    syntax_class = w2lapp.schema.syntaxes.syntax_registry.syntaxClass(self._s,nameoroid)
    try:
      attr_values = ldap.schema.Entry.__getitem__(self,nameoroid)
    except KeyError:
      attr_values = []

    # Attribute value list must contain one element
    # to display at least display one input field
    attr_values = attr_values or [None]

    result = []

    attr_type_field_name,input_field_name = {
      1:('in_attrtype','in_value'),
      0:('in_binattrtype','in_binvalue'),
    }[syntax_class.editable]

    # Eliminate binary attribute values from input form
    if not syntax_class.editable:
      attr_values = ['']

    for attr_index,attr_value in enumerate(attr_values):

      attr_inst = syntax_class(self.sid,self.form,self.ls,self.dn,self._s,nameoroid,attr_value,self.entry)

      if (
          # Attribute type 'objectClass' always read-only here
          oid=='2.5.4.0'
        ) or (
          # Attribute type 'structuralObjectClass' always read-only no matter what
          oid=='2.5.21.9'
        ) or (
          # Check whether the server indicated this attribute not to be writeable by bound identity
          not self.writeable_attr_oids is None and \
          not oid in self.writeable_attr_oids and \
          not oid in self.new_attribute_types_oids
        ) or (
          # Check whether attribute type/value is used in the RDN => not writeable
          self.existing_object_classes and \
          attr_value and \
          self.rdn_dict.has_key(nameoroid) and \
          self.rdn_dict[nameoroid].has_key(attr_value)
        ) or (
          # Set to writeable if relax rules control is in effect and attribute is NO-USER-APP in subschema
          not self.relax_rules_enabled and \
          w2lapp.schema.no_userapp_attr(self._s,oid)
        ):
        result.append('\n'.join([
          self.form.hiddenFieldHTML(attr_type_field_name,nameoroid.decode('ascii'),u''),
          w2lapp.gui.HIDDEN_FIELD % (
            input_field_name,
            self.form.utf2display(attr_inst.formValue(),sp_entity='  '),
            self.form.utf2display(attr_inst.formValue(),sp_entity='&nbsp;&nbsp;')
          )
        ]))

      else:
        attr_title = u''
        attr_type_tags = []
        attr_type_name = unicode(nameoroid).split(';')[0]
        if nameoroid_se:
          attr_type_name = unicode((nameoroid_se.names or [nameoroid_se.oid])[0],'utf-8')
          try:
            attr_title = unicode(nameoroid_se.desc or '','utf-8')
          except UnicodeError:
            # This happens sometimes because of wrongly encoded schema files
            attr_title = u''
          # Determine whether transfer syntax has to be specified with ;binary
          if nameoroid.endswith(';binary') or \
             oid in w2lapp.schema.NEEDS_BINARY_TAG or \
             nameoroid_se.syntax in w2lapp.schema.NEEDS_BINARY_TAG:
            attr_type_tags.append('binary')
        input_field = attr_inst.formField()
        input_field.name = input_field_name
        input_field.charset = self.form.accept_charset
        result.append('\n'.join([
          w2lapp.gui.HIDDEN_FIELD % (
            attr_type_field_name,
            ';'.join([attr_type_name.encode('ascii')]+attr_type_tags),
            ''
          ),
          input_field.inputHTML(
            id_value=u'_'.join((u'inputattr',attr_type_name,unicode(attr_index))).encode(self.form.accept_charset),
            title=attr_title
          )
        ]))

    return '<br>\n'.join(result)

  def fieldset_table(
    self,
    outf,
    attr_types_dict,
    fieldset_title,
  ):
    outf_lines = []
    outf_lines.append("""<fieldset title="%s">
      <legend>%s</legend>
      <table summary="%s">
      """ % (fieldset_title,fieldset_title,fieldset_title)
    )
    seen_attr_type_oids = ldap.cidict.cidict()
    attr_type_names = ldap.cidict.cidict()
    for a in self.keys():
      at_oid = self._at2key(a)[0]
      if attr_types_dict.has_key(at_oid):
        seen_attr_type_oids[at_oid] = None
        attr_type_names[a.encode('ascii')] = None
    for at_oid,at_se in attr_types_dict.items():
      if at_se and \
         not seen_attr_type_oids.has_key(at_oid) and \
         not w2lapp.schema.no_userapp_attr(self._s,at_oid):
          attr_type_names[(at_se.names or (at_se.oid,))[0].encode('ascii')] = None
    attr_types = attr_type_names.keys()
    attr_types.sort(key=str.lower)
    for i,attr_type in enumerate(attr_types):
      attr_type_name = w2lapp.gui.SchemaElementName(self.sid,self.form,self.dn,self._s,attr_type,ldap.schema.AttributeType)
      attr_value_field_html = self[attr_type]
      outf_lines.append('<tr>\n<td>\n%s\n</td>\n<td>\n%s\n</td>\n</tr>\n' % (attr_type_name,attr_value_field_html))
    outf_lines.append('</table></fieldset>')
    outf.write('\n'.join(outf_lines))
    return # fieldset_table()


def DetermineParentDN(command,dn):
  if command=='addform':
    parent_dn = dn
  elif command=='modifyform':
    parent_dn = ldaputil.base.ParentDN(dn)
  else:
    raise ValueError,"Invalid command %s" % (repr(command))
  return parent_dn # DetermineParentDN()


def SupentryDisplayString(sid,form,ls,dn,schema,command):
  if not dn:
    return ''
  supentry_display_strings = []
  inputform_supentrytemplate = w2lapp.cnf.GetParam(ls,'inputform_supentrytemplate',{})
  if inputform_supentrytemplate:
    inputform_supentrytemplate_attrtypes = set(['objectClass'])
    for oc in inputform_supentrytemplate.keys():
      inputform_supentrytemplate_attrtypes.update(GrabKeys(inputform_supentrytemplate[oc]).keys)
    try:
      parent_search_result = ls.readEntry(DetermineParentDN(command,dn),attrtype_list=list(inputform_supentrytemplate_attrtypes))
    except (ldap.NO_SUCH_OBJECT,ldap.INSUFFICIENT_ACCESS,ldap.REFERRAL):
      pass
    else:
      if parent_search_result:
        parent_entry = w2lapp.read.DisplayEntry(sid,form,ls,dn,schema,parent_search_result[0][1],'fieldSep',0)
        for oc in parent_search_result[0][1].get('objectClass',[]):
          try:
            inputform_supentrytemplate[oc]
          except KeyError:
            pass
          else:
            supentry_display_strings.append(inputform_supentrytemplate[oc] % parent_entry)
  if supentry_display_strings:
    return """
    <p title="Superior entry information">
      Superior entry:<br>
      %s
    </p>
    """ % (
      '\n'.join(supentry_display_strings),
    )
  else:
    return ''


def ObjectClassForm(
  sid,outf,form,command,ls,sub_schema,dn,rdn,
  existing_object_classes,structural_object_class
):
  """Form for choosing object class(es)"""

  ObjectClass = ldap.schema.ObjectClass
  all_oc = [
    (sub_schema.get_obj(ObjectClass,oid).names or (oid,))[0]
    for oid in sub_schema.listall(ObjectClass)
  ]

  relax_rules_enabled = ls.l._get_server_ctrls('**write**').has_key(ldapsession.CONTROL_RELAXRULES)

  command_hidden_fields = [('dn',dn)]

  if rdn:
    command_hidden_fields.append(('add_rdn',rdn))
  existing_structural_oc,existing_abstract_oc,existing_auxiliary_oc = w2lapp.schema.object_class_categories(sub_schema,existing_object_classes)
  all_structural_oc,all_abstract_oc,all_auxiliary_oc = w2lapp.schema.object_class_categories(sub_schema,all_oc)

  dit_structure_rule_html=''

  parent_dn = DetermineParentDN(command,dn)
  if parent_dn!=None and sub_schema.sed[ldap.schema.models.DITStructureRule]:
    # Determine possible structural object classes based on DIT structure rules
    # and name forms if DIT structure rules are defined in subschema
    dit_structure_ruleid = ls.getGoverningStructureRule(parent_dn,sub_schema)
    if dit_structure_ruleid!=None:
      subord_structural_ruleids,subord_structural_oc = sub_schema.get_subord_structural_oc_names(dit_structure_ruleid)
      if subord_structural_oc:
        all_structural_oc = subord_structural_oc
        dit_structure_rule_html = 'DIT structure rules:<br>%s' % ('<br>'.join(
            displayNameOrOIDList(sid,form,dn,sub_schema,subord_structural_ruleids,ldap.schema.models.DITStructureRule)
        ))

  elif parent_dn!=None and \
       '1.2.840.113556.1.4.912' in sub_schema.sed[ldap.schema.models.AttributeType] and \
       not 'OpenLDAProotDSE' in ls.rootDSE.get('objectClass',[]):
    try:
      parent_search_result = ls.readEntry(parent_dn,attrtype_list=['allowedChildClasses','allowedChildClassesEffective'])
    except (ldap.NO_SUCH_OBJECT,ldap.INSUFFICIENT_ACCESS,ldap.REFERRAL):
      pass
    else:
      if parent_search_result:
        parent_entry = parent_search_result[0][1]
        try:
          allowed_child_classes = parent_entry['allowedChildClasses']
        except KeyError:
          dit_structure_rule_html = ''
        else:
          allowed_child_classes_kind_dict = {0:[],1:[],2:[]}
          for av in allowed_child_classes:
            at_se = sub_schema.get_obj(ldap.schema.models.ObjectClass,av)
            if not at_se is None:
              allowed_child_classes_kind_dict[at_se.kind].append(av)
          all_structural_oc = allowed_child_classes_kind_dict[0]
  #        all_abstract_oc = allowed_child_classes_kind_dict[1]
  #        all_auxiliary_oc = allowed_child_classes_kind_dict[2]
          dit_structure_rule_html = 'Governed by <var>allowedChildClasses</var>.'

  existing_misc_oc = set(existing_object_classes)
  for a in existing_structural_oc+existing_abstract_oc+existing_auxiliary_oc:
    existing_misc_oc.discard(a)
  existing_misc_oc = list(existing_misc_oc)

  dit_content_rule_html = ''
  # Try to look up a DIT content rule
  if existing_object_classes and structural_object_class:
    # Determine OID of structural object class
    soc_oid = sub_schema.name2oid[ldap.schema.ObjectClass].get(structural_object_class,structural_object_class)
    dit_content_rule = sub_schema.get_obj(ldap.schema.DITContentRule,soc_oid,None)
    if dit_content_rule!=None:
      if dit_content_rule.obsolete:
        dit_content_rule_status_text = 'Ignored obsolete'
      elif relax_rules_enabled:
        dit_content_rule_status_text = 'Ignored'
      else:
        dit_content_rule_status_text = 'Governed by'
        all_auxiliary_oc_oids = set([
          sub_schema.getoid(ldap.schema.ObjectClass,nameoroid)
          for nameoroid in dit_content_rule.aux
        ])
        all_auxiliary_oc = [
          oc
          for oc in all_auxiliary_oc
          if sub_schema.getoid(ldap.schema.ObjectClass,oc) in all_auxiliary_oc_oids
        ]
      dit_content_rule_html = '%s<br>DIT content rule:<br>%s' % (
        dit_content_rule_status_text,
        w2lapp.gui.SchemaElementName(sid,form,dn,sub_schema,dit_content_rule.names[0],ldap.schema.DITContentRule)
      )

  abstract_select_field = w2lapp.form.ObjectClassSelect(
    name='ldap_oc',text='Abstract object class(es)',options=all_abstract_oc,default=existing_abstract_oc,size=20
  )
  structural_select_field = w2lapp.form.ObjectClassSelect(
    name='ldap_oc',text='Structural object class(es)',options=all_structural_oc,default=existing_structural_oc,size=20
  )
  auxiliary_select_field = w2lapp.form.ObjectClassSelect(
    name='ldap_oc',text='Auxiliary object class(es)',options=all_auxiliary_oc,default=existing_auxiliary_oc,size=20
  )
  misc_select_field = w2lapp.form.ObjectClassSelect(
    name='ldap_oc',text='Misc. object class(es)',options=[],default=existing_misc_oc,size=20
  )
  if existing_misc_oc:
    misc_select_field_th = '<th><label for="add_misc_oc">Misc.<label></th>'
    misc_select_field_td = '<td>%s</td>' % (misc_select_field.inputHTML(id_value='add_misc_oc'))
  else:
    misc_select_field_th = ''
    misc_select_field_td = ''

  # Build an select field based on config param 'addform_entry_templates'
  if command=='addform':
    addform_entry_templates_keys = w2lapp.cnf.GetParam(ls,'addform_entry_templates',{}).keys()
    addform_entry_templates_keys.append('')
    addform_entry_templates_keys.sort()
    add_template_select_field = pyweblib.forms.Select(
      'add_template',
      u'LDIF template',
      1,
      options=addform_entry_templates_keys,
      default=''
    )
    add_template_select_field.charset=form.accept_charset
    add_template_field_html = '<label for="add_template_name">%s:</label>%s' % (
      form.utf2display(add_template_select_field.text),
      add_template_select_field.inputHTML(
        default='',
        id_value='add_template_name',
        title='LDIF templates for new entries including object classes'
    ))

  else:
    add_template_field_html = ''

  Msg = {
    'addform':'Choose object class(es) for new entry or choose from quick-list.',
    'modifyform':'You may change the object class(es) for the entry.',
  }[command]

  context_menu_list = w2lapp.gui.ContextMenuSingleEntry(sid,form,ls,dn)

  # Write HTML output of object class input form
  w2lapp.gui.TopSection(
    sid,outf,form,ls,dn,
    'Object Class Selection',
    w2lapp.gui.MainMenu(sid,form,ls,dn),
    context_menu_list=context_menu_list
  )

  outf.write("""
    <div id="Input" class="Main">\n<h1>%s</h1>
      %s
      %s
      <p>
        %s
        <label for="add_form_type">Form type:</label> %s
        <input type="submit" value="Next &gt;&gt;">
      </p>
      <p>%s</p>
      <table summary="Choosing different categories of object classes for the entry">
        <tr>
          <th><label for="add_structural_oc">Structural</label></th>
          <th><label for="add_auxiliary_oc">Auxiliary</label></th>
          <th><label for="add_abstract_oc">Abstract</label></th>
          %s
        </tr>
        <tr>
          <td><label for="add_structural_oc">%s</label></td>
          <td><label for="add_auxiliary_oc">%s</label></td>
          <td><label for="add_abstract_oc">%s</label></td>
          %s
        </tr>
        <tr>
          <td>%s</td>
          <td>%s</td>
          <td>&nbsp;</td>
        </tr>
      </table>
      </form>
    </div>
  """ % (
    heading_msg[command],
    form.beginFormHTML(command,sid,'GET',None),
    ''.join([
      form.hiddenFieldHTML(param_name,param_value,u'')
      for param_name,param_value in command_hidden_fields
    ]),
    add_template_field_html,
    form.field['input_formtype'].inputHTML(id_value='add_form_type'),
    Msg,
    misc_select_field_th,
    structural_select_field.inputHTML(id_value='add_structural_oc',title='Structural object classes to be added'),
    auxiliary_select_field.inputHTML(id_value='add_auxiliary_oc',title='Auxiliary object classes to be added'),
    abstract_select_field.inputHTML(id_value='add_abstract_oc',title='Abstract object classes to be added'),
    misc_select_field_td,
    dit_structure_rule_html,
    dit_content_rule_html,
  ))
  w2lapp.gui.PrintFooter(outf,form)
  return # ObjectClassForm()


def ReadLDIFTemplate(ls,template_name):
  addform_entry_templates = w2lapp.cnf.GetParam(ls,'addform_entry_templates',{})
  if not addform_entry_templates.has_key(template_name):
    raise w2lapp.core.ErrorExit(u'LDIF template key not known.')
  ldif_file_name = addform_entry_templates[template_name]
  try:
    ldif_file = open(ldif_file_name,'r')
  except (IOError,ValueError):
    raise w2lapp.core.ErrorExit(u'Error opening LDIF template.')
  try:
    ldif_parser = ldif.LDIFRecordList(
      ldif_file,
      ignored_attr_types=[],
      max_entries=1,
      process_url_schemes=w2lapp.cnf.misc.ldif_url_schemes
    )
    ldif_parser.parse()
  except (IOError,ValueError):
    raise w2lapp.core.ErrorExit(u'Error reading/parsing LDIF template.')
  if ldif_parser.all_records:
    dn,entry = ldif_parser.all_records[0]
  else:
    raise w2lapp.core.ErrorExit(u'No entry in LDIF template.')
  return dn,entry # ReadLDIFTemplate()


def AttributeTypeDict(ls,param_name,param_default):
  """
  Build a list of attributes assumed in configuration to be constant while editing entry
  """
  attrs = ldap.cidict.cidict()
  for attr_type in w2lapp.cnf.GetParam(ls,param_name,param_default):
    attrs[attr_type] = attr_type
  return attrs # AttributeTypeDict()


def ConfiguredConstantAttributes(ls):
  """
  Build a list of attributes assumed in configuration to be constant while editing entry
  """
  return AttributeTypeDict(ls,'modify_constant_attrs',['createTimestamp','modifyTimestamp','creatorsName','modifiersName'])


def AssertionFilter(ls,entry):
  relax_rules_enabled = ls.l._get_server_ctrls('**write**').has_key(ldapsession.CONTROL_RELAXRULES)
  assertion_filter_list = []
  for attr_type in ConfiguredConstantAttributes(ls).values():
    try:
      attr_values = entry[attr_type]
    except KeyError:
      continue
    else:
      assertion_filter_list.extend([
        u'(%s)' % (u'='.join((attr_type,ldap.filter.escape_filter_chars(attr_value,escape_mode=1))))
        for attr_value in attr_values
      ])
  if assertion_filter_list:
    assertion_filter = u'(&%s)' % ''.join(assertion_filter_list)
  else:
    assertion_filter =  u'(objectClass=*)'
  return assertion_filter # AssertionFilter()


def nomatching_attrs(sub_schema,entry,allowed_attrs_dict,required_attrs_dict):
  """
  Determine attributes which does not appear in the schema but
  do exist in the entry
  """
  nomatching_attrs_dict = ldap.cidict.cidict()
  for at_name in entry.keys():
    try:
      at_oid = sub_schema.name2oid[ldap.schema.AttributeType][at_name]
    except KeyError:
      nomatching_attrs_dict[at_name] = None
    else:
      if not (
        allowed_attrs_dict.has_key(at_oid) or \
        required_attrs_dict.has_key(at_oid) or \
        at_name.lower()=='objectclass'
      ):
        nomatching_attrs_dict[at_oid] = sub_schema.get_obj(ldap.schema.AttributeType,at_oid)
  return nomatching_attrs_dict # nomatching_attrs()


def ReadOldEntry(command,ls,dn,sub_schema,assertion_filter,object_classes):
  """
  Retrieve all editable attribute types an entry
  """

  AttributeType = ldap.schema.models.AttributeType

  WRITEABLE_ATTRS_NONE                 = None
  WRITEABLE_ATTRS_SLAPO_ALLOWED        = 1
  WRITEABLE_ATTRS_GET_EFFECTIVE_RIGHTS = 2

  server_ctrls = []

  # Build a list of attributes to be requested
  read_attrs = ldap.cidict.cidict({'*':'*'})
  read_attrs.update(ConfiguredConstantAttributes(ls))
  read_attrs.update(AttributeTypeDict(ls,'requested_attrs',[]))

#  # Add the object class prefixed with @ (see RFC 4529)
#  if '1.3.6.1.4.1.4203.1.5.2' in ls.supportedFeatures:
#    for oc in object_classes or []:
#      oc_attr_str = ''.join(('@',oc))
#      read_attrs[oc_attr_str] = oc_attr_str

  # Try to request information about which attributes are writeable by the bound identity

  # Try to query attribute allowedAttributesEffective if modifying an existing entry
  if command=='modifyform' and \
     '1.2.840.113556.1.4.914' in sub_schema.sed[AttributeType]:
    # Query with attribute 'allowedAttributesEffective' e.g. on MS AD or OpenLDAP with slapo-allowed
    read_attrs['allowedAttributesEffective'] = 'allowedAttributesEffective'
    write_attrs_method = WRITEABLE_ATTRS_SLAPO_ALLOWED

#  # Try to use the Get Effective Rights Control if on OpenDS
#  elif ls.vendorVersion and ls.vendorVersion.startswith('OpenDS Directory Server') and \
#     '1.3.6.1.4.1.42.2.27.9.1.39' in sub_schema.sed[AttributeType] and \
#     object_classes:
#    required_attrs_dict,allowed_attrs_dict = sub_schema.attribute_types(object_classes,ignore_dit_content_rule=0)
#    for a in required_attrs_dict.values()+allowed_attrs_dict.values():
#      try:
#        read_attrs[a.names[0]] = a.names[0]
#      except AttributeError:
#        read_attrs[a.oid] = a.oid
#    # Query with attribute 'aclRights' with Effective Rights control e.g. on OpenDS
#    if not ls.l._get_server_ctrls('search_ext').has_key('1.3.6.1.4.1.42.2.27.9.5.2'):
#      server_ctrls.append(ldap.controls.LDAPControl('1.3.6.1.4.1.42.2.27.9.5.2',0,None))
#    read_attrs['aclRights'] = 'aclRights'
#    write_attrs_method = WRITEABLE_ATTRS_GET_EFFECTIVE_RIGHTS

  else:
    write_attrs_method = WRITEABLE_ATTRS_NONE

  assert write_attrs_method in (WRITEABLE_ATTRS_NONE,WRITEABLE_ATTRS_SLAPO_ALLOWED,WRITEABLE_ATTRS_GET_EFFECTIVE_RIGHTS),\
    ValueError("Invalid value for write_attrs_method" )

  # Explicitly request attribute 'ref' if in manage DSA IT mode
  if ls.l._get_server_ctrls('**all**').has_key(ldapsession.CONTROL_MANAGEDSAIT):
    read_attrs['ref'] = 'ref'

  # Read the editable attribute values of entry
  try:
    ldap_entry = ls.readEntry(
      dn,
      read_attrs.values(),
      search_filter=(assertion_filter or u'(objectClass=*)').encode(ls.charset),
      no_cache=1,
      server_ctrls=server_ctrls or None,
    )[0][1]
  except IndexError:
    raise ldap.NO_SUCH_OBJECT

  entry = UserEditableEntry(ls,sub_schema,dn.encode(ls.charset),ldap_entry)

  if write_attrs_method==WRITEABLE_ATTRS_NONE:
    # No method to determine writeable attributes was used
    writeable_attr_oids = None

  elif write_attrs_method==WRITEABLE_ATTRS_SLAPO_ALLOWED:
    # Determine writeable attributes from attribute 'allowedAttributesEffective'
    try:
      writeable_attr_oids = set([
        sub_schema.getoid(AttributeType,a).decode('ascii')
        for a in entry['allowedAttributesEffective']
      ])
    except KeyError:
      writeable_attr_oids = set([])
    else:
      del entry['allowedAttributesEffective']

  elif write_attrs_method==WRITEABLE_ATTRS_GET_EFFECTIVE_RIGHTS:
    # Try to determine writeable attributes from attribute 'aclRights'
    acl_rights_attribute_level = [
      (a,v)
      for a,v in entry.data.items()
      if a[0]=='1.3.6.1.4.1.42.2.27.9.1.39' and a[1]=='attributelevel'
    ]
    if acl_rights_attribute_level:
      writeable_attr_oids = set([])
      for a,v in acl_rights_attribute_level:
        try:
          dummy1,dummy2,attr_type = a
        except ValueError:
          pass
        else:
          if v[0].lower().find(',write:1,')>=0:
            writeable_attr_oids.add(sub_schema.getoid(AttributeType,a[2]).decode('ascii'))
        del entry[';'.join((dummy1,dummy2,attr_type))]

  return entry,writeable_attr_oids # ReadOldEntry()


def MultiValueAttributeTypes(sub_schema,input_form_entry,required_attrs_dict,allowed_attrs_dict):
  # Build two attribute type lists for normal input and file input
  all_txtattrtypes = []
  all_binattrtypes = []
  all_attrs_dict = {}
  object_class_oids = ldap.cidict.cidict(input_form_entry.get_object_class_oids())
  # Special treatment for object class extensibleObject
  if object_class_oids.has_key('1.3.6.1.4.1.1466.101.120.111') or \
     object_class_oids.has_key('extensibleObject'):
    all_attrs_dict.update(sub_schema.sed[ldap.schema.models.AttributeType])
  else:
    all_attrs_dict.update(required_attrs_dict)
    all_attrs_dict.update(allowed_attrs_dict)
  # Sort out all non-editable attribute types including 'objectClass'
  for oid,a in all_attrs_dict.items():
    if not a or a.oid=='2.5.4.0' or a.single_value or a.collective or \
       w2lapp.schema.no_userapp_attr(sub_schema,a.oid):
      del all_attrs_dict[oid]
  # Sort into textual and binary attributes
  for a in all_attrs_dict.values():
    a2 = (a.names or (a.oid,))[0]
    if w2lapp.schema.no_humanreadable_attr(sub_schema,a.oid):
      all_binattrtypes.append(a2)
    else:
      all_txtattrtypes.append(a2)
  all_attrtypes = all_attrs_dict.keys()
  all_attrtypes.sort(key=str.lower)
  all_txtattrtypes.sort()
  all_binattrtypes.sort(key=str.lower)
  return all_txtattrtypes,all_binattrtypes # EditableAttributeTypeLists()


def WriteInputFormFooter(outf,form,sub_schema,input_form_entry,required_attrs_dict,allowed_attrs_dict,input_formtype):

  all_txtattrtypes,all_binattrtypes = MultiValueAttributeTypes(sub_schema,input_form_entry,required_attrs_dict,allowed_attrs_dict)

  all_attrtypes = ['']
  all_attrtypes.extend(all_txtattrtypes)
  all_attrtypes.extend(all_binattrtypes)
  all_attrtypes.sort(key=str.lower)

  if all_txtattrtypes:
    all_txtattrtypes_html = """        <tr><td title="Choose additional textual attribute type">
          <select name="input_addattrtype"><option>%s</option></select>
          <input name="input_formtype" value="+" type="submit">
        </td></tr>
    """ % ('</option>\n<option>'.join(all_txtattrtypes))
  else:
    all_txtattrtypes_html = ''

  if all_binattrtypes:
    all_binattrtypes_html = """
        <tr><td title="Additional binary attribute type and value">
          <select name="in_binattrtype"><option>%s</option></select>
          <input type="file" name="in_binvalue">
        </td></tr>
    """ % ('</option>\n<option>'.join(all_binattrtypes))
  else:
    all_binattrtypes_html = ''

  outf.write("""
    <fieldset>
      <legend>Add more values to multi-valued attributes</legend>
      <table summary="More values to multi-valued attributes">
        %s
        %s
        <tr><td title="Additional textual attribute type and value">
          <input name="in_attrtype" size="15" maxlength="200" value="">
          <input name="in_value" size="40" value="">
        </td></tr>
        <tr><td title="Additional binary attribute type and value">
          <input name="in_binattrtype" size="15" maxlength="200" value="">
          <input type="file" name="in_binvalue" size="40" maxlength="%d" value="">
        </td></tr>
      </table>
    </fieldset>
""" % (
    all_txtattrtypes_html,
    all_binattrtypes_html,
    w2lapp.cnf.misc.input_maxfilelen,
  ))


def WriteLDIFField(outf,form,ls,sub_schema,dn,entry):
  f = StringIO()
  ldif_writer = ldif.LDIFWriter(f)
  ldap_entry = {}
  for attr_type,attr_values in entry.items():
    if not w2lapp.schema.no_userapp_attr(sub_schema,attr_type):
      ldap_entry[attr_type] = [
        attr_value
        for attr_value in attr_values
      ]
  ldif_writer.unparse(dn.encode(ls.charset),ldap_entry)
  outf.write("""
    <fieldset>
      <legend>Raw LDIF data</legend>
      <textarea name="in_ldif" rows="50" cols="80" wrap="off">\n%s</textarea>
      <p>
        Notes:
      </p>
      <ul>
        <li>Lines containing "dn:" will be ignored</li>
        <li>Only the first entry (until first empty line) will be accepted</li>
        <li>Maximum length is set to %d bytes</li>
        <li>Allowed URL schemes: %s</li>
      </ul>
    </fieldset>
""" % (
    form.utf2display(f.getvalue().decode('utf-8'),sp_entity='  ',lf_entity='\n'),
    w2lapp.cnf.misc.ldif_maxbytes,
    ', '.join(w2lapp.cnf.misc.ldif_url_schemes))
  )
  return # WriteLDIFField()


def w2l_AddForm(sid,outf,command,form,ls,dn,Msg='',rdn_default='',entry=None,skip_oc_input=0):

  if Msg:
    Msg = '<p class="ErrorMessage">%s</p>' % (Msg)

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

  if 'ldap_oc' in form.inputFieldNames:
    # Read objectclass(es) from input form
    entry = {'objectClass':[
      oc.encode('ascii')
      for oc in form.field['ldap_oc'].value
    ]}
    add_rdn = None
    add_basedn = form.getInputValue('add_basedn',[dn])[0]

  elif 'add_template' in form.inputFieldNames:
    add_dn,entry = ReadLDIFTemplate(ls,form.field['add_template'].value[0])
    add_rdn,add_basedn = ldaputil.base.SplitRDN(add_dn.decode(ls.charset))
    add_basedn = add_basedn or dn

  elif not skip_oc_input:
    # Output the web page with object class input form
    ObjectClassForm(
      sid,outf,form,'addform',ls,sub_schema,dn,
      form.getInputValue('add_rdn',[rdn_default])[0],
      [],None
    )
    return

  else:
    add_rdn = form.getInputValue('add_rdn',[rdn_default])[0]
    add_basedn = form.getInputValue('add_basedn',[dn])[0]


  entry = UserEditableEntry(ls,sub_schema,dn.encode(ls.charset),entry or {})

  rdn_options = entry.get_rdn_templates()

  supentry_display_string = SupentryDisplayString(sid,form,ls,add_basedn,sub_schema,command)

  input_formtype = form.getInputValue(
    'input_formtype',
    form.getInputValue(
      'input_currentformtype',['Template']
    )
  )[0]

  # Check whether to fall back to table input mode
  if input_formtype==u'Template':
    template_oc,read_template_dict = w2lapp.read.DetermineHTMLTemplates(ls,entry,'input_template')
    if not template_oc:
      Msg = ''.join((Msg,'<p class="WarningMessage">No templates defined for chosen object classes.</p>'))
      input_formtype=u'Table'

  if entry.get_structural_oc() is None:
    Msg = ''.join((Msg,'<p class="WarningMessage">You did not choose a STRUCTURAL object class!</p>'))

  required_attrs_dict,allowed_attrs_dict = entry.attribute_types()
  nomatching_attrs_dict = nomatching_attrs(sub_schema,entry,allowed_attrs_dict,required_attrs_dict)

  if rdn_options and len(rdn_options)>0:
    # <select> field
    rdn_input_field = pyweblib.forms.Select('add_rdn','RDN variants',1,options=rdn_options)
  else:
    # Just a normal <input> text field
    rdn_input_field = form.field['add_rdn']
  if add_rdn:
    rdn_input_field.setDefault(add_rdn)
  else:
    rdn_candidate_attr_nameoroids = [
      (required_attrs_dict[at_oid].names or (at_oid,))[0]
      for at_oid in required_attrs_dict.keys()
      if at_oid!='2.5.4.0' and not w2lapp.schema.no_humanreadable_attr(sub_schema,at_oid)
    ]
    if len(rdn_candidate_attr_nameoroids)==1:
      rdn_input_field.setDefault(rdn_candidate_attr_nameoroids[0]+'=')

  w2lapp.gui.TopSection(
    sid,outf,form,ls,dn,
    heading_msg[command],
    w2lapp.gui.MainMenu(sid,form,ls,dn),
    context_menu_list=w2lapp.gui.ContextMenuSingleEntry(sid,form,ls,dn)
  )

  outf.write("""<div id="Input" class="Main">
    <h1>%s</h1>
    %s\n%s\n%s\n%s\n%s\n
    <input type="submit" value="Add">
    Change input form:
    <input type="submit" name="input_formtype" value="Template">
    <input type="submit" name="input_formtype" value="Table">
    <input type="submit" name="input_formtype" value="LDIF">
    <p>RDN: %s</p>
    """ % (
      heading_msg[command],
      Msg,
      supentry_display_string,
      form.beginFormHTML('add',sid,'POST',target=None,enctype='multipart/form-data'),
      form.hiddenFieldHTML('dn',dn,u''),
      form.hiddenFieldHTML('add_basedn',add_basedn,u''),
      rdn_input_field.inputHTML(),
    )
  )

  input_form_entry = InputFormEntry(sid,form,ls,dn,sub_schema,entry,None)

  # Use HTML templates for displaying a partial input form
  if input_formtype==u'Template':
    displayed_attrs = w2lapp.read.PrintTemplateOutput(
      sid,outf,form,ls,sub_schema,entry,
      input_form_entry,'input_template',
      display_duplicate_attrs=False
    )
    outf_lines = []
    for attr_type,attr_values in entry.items():
      at_oid = entry._at2key(attr_type)[0]
      if not at_oid in displayed_attrs:
        syntax_class = w2lapp.schema.syntaxes.syntax_registry.syntaxClass(sub_schema,attr_type)
        for attr_value in attr_values:
          attr_inst = syntax_class(sid,form,ls,dn,sub_schema,attr_type,attr_value,entry)
          outf_lines.append(form.hiddenFieldHTML('in_attrtype',attr_type.decode('ascii'),u''))
          try:
            attr_value_html = form.utf2display(attr_inst.formValue(),sp_entity='  ')
          except UnicodeDecodeError:
            # Simply display an empty string if anything goes wrong with Unicode decoding (e.g. with binary attributes)
            attr_value_html = ''
          outf_lines.append(w2lapp.gui.HIDDEN_FIELD % (
            'in_value',attr_value_html,''
          ))
    outf.write(''.join(outf_lines))

  elif input_formtype==u'Table':
    # Displaying input form as simple list
    input_form_entry.fieldset_table(outf,required_attrs_dict,'Required attributes')
    input_form_entry.fieldset_table(outf,allowed_attrs_dict,'Allowed attributes')
    if nomatching_attrs_dict:
      input_form_entry.fieldset_table(outf,nomatching_attrs_dict,'Attributes not matching schema')

  elif input_formtype==u'LDIF':
    WriteLDIFField(outf,form,ls,sub_schema,dn,entry)

  outf.write(form.hiddenFieldHTML('input_currentformtype',input_formtype,u''))
  outf.write('</form></div>')
  w2lapp.gui.PrintFooter(outf,form)
  return # w2l_AddForm()


def w2l_ModifyForm(sid,outf,command,form,ls,dn,Msg='',entry=None,skip_oc_input=0,writeable_attr_oids=None):

  if Msg:
    Msg = '<p class="ErrorMessage">%s</p>' % (Msg)

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

  if 'ldap_oc' in form.inputFieldNames:
    # Read objectclass(es) from input form

    new_object_classes = [ oc.encode('ascii') for oc in form.field['ldap_oc'].value ]

    entry,read_writeable_attr_oids = ReadOldEntry(command,ls,dn,sub_schema,None,new_object_classes)
    if not read_writeable_attr_oids is None:
      writeable_attr_oids = read_writeable_attr_oids

    existing_object_classes = entry['objectClass'][:]
    existing_object_classes_oid_set = set([
      sub_schema.getoid(ldap.schema.ObjectClass,oc_name) for oc_name in existing_object_classes
    ])
    new_object_classes_oid_set = set([
      sub_schema.getoid(ldap.schema.ObjectClass,oc_name) for oc_name in new_object_classes
    ])
    entry['objectClass'] = []
    for oc_name in existing_object_classes:
      oc_oid = sub_schema.getoid(ldap.schema.ObjectClass,oc_name)
      if oc_oid in new_object_classes_oid_set:
        entry['objectClass'].append(oc_name)
    for oc_name in new_object_classes:
      oc_oid = sub_schema.getoid(ldap.schema.ObjectClass,oc_name)
      if not oc_oid in existing_object_classes_oid_set:
        entry['objectClass'].append(oc_name)

  elif not skip_oc_input:
    # Output the web page with object class input form
    objectClass,structuralObjectClass = ls.getObjectClasses(dn)
    if structuralObjectClass is None:
      structuralObjectClass = sub_schema.get_structural_oc(objectClass)
    ObjectClassForm(
      sid,outf,form,command,ls,sub_schema,dn,
      None,objectClass,structuralObjectClass
    )
    return
  else:
    existing_object_classes = ls.getObjectClasses(dn)[0]
    writeable_attr_oids = form.getInputValue('in_wrtattroids',[])
    if writeable_attr_oids==[u'nonePseudoValue;x-web2ldap-None']:
      writeable_attr_oids = None

  input_form_entry = InputFormEntry(sid,form,ls,dn,sub_schema,entry,writeable_attr_oids,existing_object_classes)
  required_attrs_dict,allowed_attrs_dict = input_form_entry.entry.attribute_types()
  nomatching_attrs_dict = nomatching_attrs(sub_schema,input_form_entry.entry,allowed_attrs_dict,required_attrs_dict)

  supentry_display_string = SupentryDisplayString(sid,form,ls,dn,sub_schema,command)

  input_formtype = form.getInputValue(
    'input_formtype',
    form.getInputValue(
      'input_currentformtype',['Template']
    )
  )[0]

  # Check whether to fall back to table input mode
  if input_formtype==u'Template':
    template_oc,read_template_dict = w2lapp.read.DetermineHTMLTemplates(ls,entry,'input_template')
    if not template_oc:
      Msg = ''.join((Msg,'<p class="WarningMessage">No templates defined for chosen object classes.</p>'))
      input_formtype=u'Table'

  if writeable_attr_oids is None:
    in_wrtattroids_values = w2lapp.gui.HIDDEN_FIELD % ('in_wrtattroids','nonePseudoValue;x-web2ldap-None','')
  else:
    in_wrtattroids_values = ''.join([
      w2lapp.gui.HIDDEN_FIELD % ('in_wrtattroids',form.utf2display(at_name,sp_entity='  '),'')
      for at_name in writeable_attr_oids or []
    ])

  w2lapp.gui.TopSection(
    sid,outf,form,ls,dn,
    heading_msg[command],
    w2lapp.gui.MainMenu(sid,form,ls,dn),
    context_menu_list=w2lapp.gui.ContextMenuSingleEntry(sid,form,ls,dn)
  )

  outf.write("""<div id="Input" class="Main">
    <h1>%s</h1>
    %s\n%s\n%s\n%s\n
    <input type="submit" value="Modify">
    Change input form:
    <input type="submit" name="input_formtype" value="Template">
    <input type="submit" name="input_formtype" value="Table">
    <input type="submit" name="input_formtype" value="LDIF">
    %s\n%s\n
    <br>
    """ % (
      heading_msg[command],
      Msg,
      supentry_display_string,
      form.beginFormHTML('modify',sid,'POST',target=None,enctype='multipart/form-data'),
      form.hiddenFieldHTML('dn',dn,u''),
      ''.join([
        form.hiddenFieldHTML('in_oldattrtypes',at_name.decode('ascii'),u'')
        for at_name in entry.keys()
      ]),
      in_wrtattroids_values,
    )
  )

  # Use HTML templates for displaying a partial input form
  if input_formtype=='Template':
    displayed_attrs = w2lapp.read.PrintTemplateOutput(sid,outf,form,ls,sub_schema,entry,input_form_entry,'input_template')
    # Output all existing attributes as hidden fields which were not yet displayed
    # through HTML templates
    outf_lines = []
    for attr_type,attr_values in entry.items():
      at_oid = entry._at2key(attr_type)[0]
      if not at_oid in displayed_attrs:
        syntax_class = w2lapp.schema.syntaxes.syntax_registry.syntaxClass(sub_schema,attr_type)
        for attr_value in attr_values:
          if syntax_class.editable:
            attr_inst = syntax_class(sid,form,ls,dn,sub_schema,attr_type,attr_value,entry)
            outf_lines.append(w2lapp.gui.HIDDEN_FIELD % (
              'in_attrtype',form.utf2display(unicode(attr_type,'ascii'),sp_entity='  '),''
            ))
            outf_lines.append(w2lapp.gui.HIDDEN_FIELD % (
              'in_value',form.utf2display(attr_inst.formValue(),sp_entity='  '),''
            ))
    outf.write(''.join(outf_lines))

  elif input_formtype=='Table':
    # Displaying rest of input form as simple list
    input_form_entry.fieldset_table(outf,required_attrs_dict,'Required attributes')
    input_form_entry.fieldset_table(outf,allowed_attrs_dict,'Allowed attributes')
    if nomatching_attrs_dict:
      input_form_entry.fieldset_table(outf,nomatching_attrs_dict,'Existing attributes not matching schema')

  elif input_formtype=='LDIF':
    WriteLDIFField(outf,form,ls,sub_schema,dn,entry)

  assertion_filter = AssertionFilter(ls,entry)

  outf.write("""
  %s
  %s
  </form>
  </div>
  """ % (
    form.hiddenFieldHTML('in_assertion',assertion_filter,u''),
    form.hiddenFieldHTML('input_currentformtype',unicode(input_formtype),u''),
  ))
  w2lapp.gui.PrintFooter(outf,form)
  return # w2l_ModifyForm()
