# -*- coding: utf-8 -*-
"""
w2lapp.schema.syntaxes: classes for known attribute types

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,imghdr,sndhdr,urllib,uuid,datetime,time,utctime,ldap,ldapurl,pyweblib.forms, \
       ldaputil.base,ldap.schema,xml.etree.ElementTree, \
       w2lapp.viewer,w2lapp.form,w2lapp.gui,mspki.util,w2lapp.cnf,ldaputil.schema

from types import StringType,UnicodeType,IntType,LongType,FloatType,ClassType

from ldaputil.base import is_dn

try:
  from netaddr import IPAddress,IPNetwork
except ImportError:
  from ipaddr import IPAddress,IPNetwork

# Detect Python Imaging Library (PIL)
try:
  from PIL import Image as PILImage
except ImportError:
  PILImage = None

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

try:
  from ldapoidreg import oid as oid_desc_reg
except ImportError:
  oid_desc_reg = {}


class SyntaxRegistry:

  def __init__(self):
    self.oid2syntax = ldap.cidict.cidict()
    self.at2syntax = ldap.cidict.cidict()

  def registerSyntaxClass(self,c):
    if type(c) is ClassType and hasattr(c,'oid'):
      self.oid2syntax[c.oid] = c

  def registerAttrType(self,syntax_oid,attrTypes):
    for a in attrTypes:
      self.at2syntax[a.strip()] = syntax_oid

  def syntaxClass(self,schema,attrtype_nameoroid):
    if attrtype_nameoroid is None:
      return LDAPSyntax
    attrtype_oid = schema.getoid(ldap.schema.AttributeType,attrtype_nameoroid)
    if self.at2syntax.has_key(attrtype_oid):
      syntax_class = self.oid2syntax[self.at2syntax[attrtype_oid]]
    else:
      attrtype_se = schema.get_inheritedobj(ldap.schema.AttributeType,attrtype_oid,['syntax'])
      if attrtype_se is None or attrtype_se.syntax is None:
        syntax_class = LDAPSyntax
      else:
        syntax_class = self.oid2syntax.get(attrtype_se.syntax,LDAPSyntax)
    return syntax_class

  def attrInstance(self,sid,form,ls,dn,schema,attrType,attrValue,entry=None):
    syntax_class = self.syntaxClass(schema,attrType)
    attr_instance = syntax_class(sid,form,ls,dn,schema,attrType,attrValue,entry)
    return attr_instance


url_pattern  = r'^(ftp|http|https|news|snews|ldap|ldaps|mailto):(|//)[^ ]*'
url_regex  = re.compile(url_pattern)
labeleduri_regex = re.compile(url_pattern+r' .*')
timestamp_pattern = r'^([0-9]){12,14}((\.|,)[0-9]+)*(Z|(\+|-)[0-9]{4})$'
timestamp_regex  = re.compile(timestamp_pattern)
mail_pattern = r'^[\w@.+=/_ ()-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$'
mail_regex = re.compile(mail_pattern)


####################################################################
# Classes of known syntaxes
####################################################################


class LDAPSyntaxValueError(ValueError):
  pass


class LDAPSyntaxRegexNoMatch(LDAPSyntaxValueError):
  pass


class LDAPSyntax:
  oid=''
  desc='Any LDAP syntax'
  inputSize = 50
  maxLen = w2lapp.cnf.misc.input_maxfieldlen
  maxValues = w2lapp.cnf.misc.input_maxattrs
  mimeType = 'application/octet-stream'
  fileExt = 'bin'
  editable = 1
  reObj = None
  searchSep = '<br>'
  readSep = '<br>'
  fieldSep = '<br>'

  def __init__(self,sid,form,ls,dn,schema,attrType,attrValue,entry=None):
    self.attrType = attrType
    assert type(attrValue)==StringType or attrValue is None, "Argument 'attrValue' must be StringType or None"
    self.attrValue = attrValue
    self._sid = sid
    self._form = form
    self._ls = ls
    self._schema = schema
    assert type(dn)==UnicodeType, "Argument 'dn' must be UnicodeType"
    self._dn = dn
    assert entry is None or isinstance(entry,ldaputil.schema.Entry), \
      TypeError('entry must be ldaputil.schema.Entry but is %s' % (entry.__class__.__name__))
    self._entry = entry or ldaputil.schema.Entry(self._schema,None,{})

  def setAttrValue(self,attrValue):
    self.validate(attrValue)
    self.attrValue = attrValue

  def sanitizeInput(self,attrValue):
    return attrValue

  def _regexValidate(self,attrValue):
    if self.reObj and (self.reObj.match(attrValue) is None):
      raise LDAPSyntaxRegexNoMatch, \
        "Class %s: %s does not match pattern %s." % (
          self.__class__.__name__,repr(attrValue[0:]),repr(self.reObj.pattern)
        )
    return # _regexValidate()

  def _validate(self,attrValue):
    return True

  def validate(self,attrValue):
    if attrValue:
      if not self._validate(attrValue):
        raise LDAPSyntaxValueError, \
          "Class %s: %s does not comply to syntax (attr type %s)." % (
            self.__class__.__name__,repr(attrValue),repr(self.attrType)
          )
      self._regexValidate(attrValue)

  def formValue(self):
    try:
      result = self._ls.uc_decode(self.attrValue or '')[0]
    except UnicodeDecodeError:
      result = u'!!!snipped because of UnicodeDecodeError!!!'
    return result

  def formField(self):
    input_field = pyweblib.forms.Input(
      self.attrType,
      ': '.join([self.attrType,self.desc]),
      self.maxLen,self.maxValues,None,default=None,size=min(self.maxLen,self.inputSize)
    )
    input_field.charset = self._form.accept_charset
    input_field.setDefault(self.formValue())
    return input_field

  def getMimeType(self):
    return self.mimeType

  def displayValue(self,valueindex=0,commandbutton=0):
    if ldapurl.isLDAPUrl(self.attrValue):
      displayer_class = LDAPUrl
    elif url_regex.search(self.attrValue)!=None:
      displayer_class = Uri
    elif timestamp_regex.match(self.attrValue)!=None:
      displayer_class = GeneralizedTime
    elif mail_regex.match(self.attrValue)!=None:
      displayer_class = RFC822Address
    else:
      displayer_class = DirectoryString
    # Crude hack
    self_class = self.__class__
    self.__class__ = displayer_class
    result = displayer_class.displayValue(self,valueindex,commandbutton)
    self.__class__ = self_class
    return result


class Binary(LDAPSyntax):
  oid = '1.3.6.1.4.1.1466.115.121.1.5'
  desc = 'Binary'
  editable = 0

  def formField(self):
    f = pyweblib.forms.File(
      self.attrType,
      ': '.join([self.attrType,self.desc]),
      self.maxLen,self.maxValues,None,default=self.attrValue,size=50
    )
    f.mimeType = self.mimeType
    return f

  def displayValue(self,valueindex=0,commandbutton=0):
    return '%d bytes | %s' % (
      len(self.attrValue),
      self._form.applAnchor(
        'read','View/Load',self._sid,
        [('dn',self._dn),('read_attr',self.attrType),('read_attrindex',str(valueindex))]
      )
    )


class Audio(Binary):
  oid = '1.3.6.1.4.1.1466.115.121.1.4'
  desc = 'Audio'
  mimeType = 'audio/basic'
  fileExt = 'au'

  def _validate(self,attrValue):
    f = StringIO(attrValue)
    res = sndhdr.test_au(attrValue,f)
    return res!=None

  def displayValue(self,valueindex=0,commandbutton=0):
    mimetype = self.getMimeType()
    return """
      <embed
        type="%s"
        autostart="false"
        src="%s/read/%s?dn=%s&amp;read_attr=%s&amp;read_attrindex=%d"
      >
      %d bytes of audio data (%s)
      """ % (
        mimetype,
        self._form.script_name,self._sid,
        urllib.quote(self._dn.encode(self._form.accept_charset)),
        urllib.quote(self.attrType),
        valueindex,
        len(self.attrValue),
        mimetype
      )


class AttributeCertificate(Binary):
  oid = '1.3.6.1.4.1.4203.666.11.10.2.1'
  desc = 'X.509 Attribute Certificate'
  mimeType = 'application/pkix-attr-cert'
  fileExt = 'cer'


class DirectoryString(LDAPSyntax):
  oid = '1.3.6.1.4.1.1466.115.121.1.15'
  desc = 'Directory String'

  def _validate(self,attrValue):
    try:
      self._ls.uc_encode(self._ls.uc_decode(attrValue)[0])[0]
    except UnicodeError:
      return False
    else:
      return True

  def displayValue(self,valueindex=0,commandbutton=0):
    return self._form.utf2display(self._ls.uc_decode(self.attrValue)[0])


class DistinguishedName(DirectoryString):
  oid = '1.3.6.1.4.1.1466.115.121.1.12'
  desc = 'Distinguished Name'
  isBindDN = False

  def _validate(self,attrValue):
    return is_dn(self._ls.uc_decode(attrValue)[0])

  def displayValue(self,valueindex=0,commandbutton=0):
    return w2lapp.gui.DisplayDN(
      self._sid,
      self._form,
      self._ls,
      self._ls.uc_decode(self.attrValue)[0],
      commandbutton=commandbutton,
      bindas_button=self.isBindDN,
    )


class BindDN(DistinguishedName):
  oid = 'BindDN-oid'
  desc = 'A Distinguished Name used to bind to a directory'
  isBindDN = True


class NameAndOptionalUID(DistinguishedName):
  oid = '1.3.6.1.4.1.1466.115.121.1.34'
  desc = 'Name And Optional UID'

  def _splitDNandUID(self,v):
    try:
      sep_ind = v.rindex(u'#')
    except ValueError:
      dn = v
      uid = None
    else:
      dn = v[0:sep_ind]
      uid = v[sep_ind+1:]
    return dn,uid

  def _validate(self,attrValue):
    dn,uid = self._splitDNandUID(self._ls.uc_decode(attrValue)[0])
    return is_dn(dn)

  def displayValue(self,valueindex=0,commandbutton=0):
    value = self.attrValue.split('#')
    dn_str = w2lapp.gui.DisplayDN(
      self._sid,self._form,self._ls,self._ls.uc_decode(self.attrValue)[0],commandbutton=commandbutton
    )
    if len(value)==1 or not value[1]:
      return dn_str
    else:
      return ' | '.join([self._form.utf2display(self._ls.uc_decode(value[1])),dn_str])


class CaseExactString(DirectoryString):
  oid = '1.3.6.1.4.1.1466.115.121.1.26'
  desc = 'Case Exact String'


class BitString(DirectoryString):
  oid = '1.3.6.1.4.1.1466.115.121.1.6'
  desc = 'Bit String'
  reObj=re.compile("^'[01]+'B$")


class GeneralizedTime(LDAPSyntax):
  oid = '1.3.6.1.4.1.1466.115.121.1.24'
  desc = 'Generalized Time'
  inputSize = 16
  maxLen=22
  reObj=timestamp_regex
  timeDefault = None
  acceptableDateformats = (
    r'%Y-%m-%d',
    r'%d.%m.%Y',
    r'%m/%d/%Y',
  )

  def _validate(self,attrValue):
    # Extract and validate time zone information
    if attrValue.endswith('Z'):
      # UTC Time (Zulu Time)
      attrValue = attrValue[:-1]
    elif (attrValue[-5]=='+' or attrValue[-5]=='-'):
      # Found + or - as time zone separator
      tzstr = attrValue[-4:]
      if len(tzstr)!=4:
        return 0
      else:
        try:
          int(tzstr)
        except ValueError:
          return 0
      attrValue = attrValue[:-5]
    else:
      # time zone part is missing
      return 0
    attrValue = attrValue.replace(',','.')
    # Extract and validate date and time information
    if '.' in attrValue:
      # There seems to be a fraction part
      try:
        datetime.datetime.strptime(attrValue,r'%Y%m%d%H%M%S.%f')
      except ValueError:
        return 0
      else:
        return 1
    else:
      # no fraction part
      try:
        datetime.datetime.strptime(attrValue,r'%Y%m%d%H%M%S')
      except ValueError:
        return 0
      else:
        return 1

  def sanitizeInput(self,attrValue):
    attrValue = attrValue.strip()
    if self.timeDefault and self.acceptableDateformats:
      for time_format in self.acceptableDateformats:
        try:
          time_tuple = datetime.datetime.strptime(attrValue,time_format)
        except ValueError:
          result = attrValue
        else:
          result = datetime.datetime.strftime(time_tuple,r'%Y%m%d'+self.timeDefault+'Z')
          break
    else:
      result = attrValue
    return result # sanitizeInput()

  def displayValue(self,valueindex=0,commandbutton=0):
    try:
      datetime_str = utctime.strftimeiso8601(utctime.strptime(self.attrValue)).decode('ascii')
    except (ValueError,UnicodeError):
      datetime_str = self._ls.uc_decode(self.attrValue)[0]
    datetime_str = self._form.utf2display(datetime_str)
    return datetime_str


class UTCTime(GeneralizedTime):
  oid = '1.3.6.1.4.1.1466.115.121.1.53'
  desc = 'UTC Time'


class IA5String(DirectoryString):
  oid = '1.3.6.1.4.1.1466.115.121.1.26'
  desc = 'IA5 String'
  charset = 'ascii'


class NullTerminatedDirectoryString(DirectoryString):
  oid = 'NullTerminatedDirectoryString-oid'
  desc = 'Directory String terminated by null-byte'

  def sanitizeInput(self,attrValue):
    return attrValue+chr(0)

  def _validate(self,attrValue):
    return attrValue.endswith(chr(0))

  def formValue(self):
    return self._ls.uc_decode((self.attrValue or chr(0))[:-1])[0]

  def displayValue(self,valueindex=0,commandbutton=0):
    return self._form.utf2display(self._ls.uc_decode((self.attrValue or chr(0))[:-1])[0])


class OtherMailbox(DirectoryString):
  oid = '1.3.6.1.4.1.1466.115.121.1.39'
  desc = 'Other Mailbox'
  charset = 'ascii'


class Integer(IA5String):
  oid = '1.3.6.1.4.1.1466.115.121.1.27'
  desc = 'Integer'
  inputSize = 12

  def sanitizeInput(self,attrValue):
    try:
      return str(int(attrValue))
    except ValueError:
      return attrValue

  def _validate(self,attrValue):
    try:
      int(attrValue)
    except ValueError:
      return False
    else:
      return True


class IntegerRange(Integer):
  oid = 'IntegerRange-oid'
  desc = 'Integer within a certain range'
  # The following class members are set to None to make direct use of this class fail
  minValue = None
  maxValue = None

  def __init__(self,sid,form,ls,dn,schema,attrType,attrValue,entry=None):
    Integer.__init__(self,sid,form,ls,dn,schema,attrType,attrValue,entry)
    if self.maxValue!=None:
      self.maxLen = len(str(self.maxValue))

  def _validate(self,attrValue):
    try:
      intValue = int(attrValue)
    except ValueError:
      return False
    else:
      return (intValue>=self.minValue) and (intValue<=self.maxValue)

  def formField(self):
    form_value = self.formValue()
    max_len = max(len(form_value),len(str(self.minValue)),len(str(self.maxValue)))
    return pyweblib.forms.Input(
      self.attrType,
      ': '.join([self.attrType,self.desc]),
      max_len,self.maxValues,'[0-9]*',default=form_value,size=max_len
    )


class IPHostAddress(IA5String):
  oid = 'IPAddress-oid'
  desc = 'string representation of IPv4 or IPv6 address or network'
  # Class in module ipaddr which parses address/network values

  def sanitizeInput(self,attrValue):
    return attrValue.strip()

  def _validate(self,attrValue):
    try:
      _ = IPAddress(attrValue)
    except ValueError,e:
      return False
    else:
      return True


class IPNetworkAddress(IPHostAddress):
  oid = 'IPAddress-oid'
  desc = 'string representation of IPv4 or IPv6 address'

  def _validate(self,attrValue):
    try:
      _ = IPNetwork(attrValue)
    except ValueError,e:
      return False
    else:
      return True


class IPServicePortNumber(IntegerRange):
  oid = 'IPServicePortNumber-oid'
  desc = 'Port number for an UDP- or TCP-based service'
  minValue = 0
  maxValue = 65535


class MacAddress(IA5String):
  oid = 'MacAddress-oid'
  desc = 'MAC address in hex-colon notation'
  minLen = 17
  maxLen = 17
  reObj=re.compile(r'^([0-9a-f]{2}\:){5}[0-9a-f]{2}$')
  nosepReObj = re.compile(r'^[0-9a-f]{12}$')

  def sanitizeInput(self,attrValue):
    attrValue = attrValue.strip().lower().replace('.','').replace('.','').replace(':','')
    result = ':'.join([
      attrValue[2*i:2*i+2]
      for i in range(6)
    ])
    return result


class RFC822Address(IA5String):
  oid = 'RFC822Address-oid'
  desc = 'RFC 822 mail address'
  reObj = re.compile(mail_pattern)

  def displayValue(self,valueindex=0,commandbutton=0):
    mailadr = self._ls.uc_decode(self.attrValue)[0]
    return '<a href="mailto:%s">%s</a>' % (
      self._form.utf2display(mailadr),
      self._form.utf2display(mailadr)
    )

class Uri(DirectoryString):
  """
  see RFC 2079
  """
  oid = 'Uri-OID'
  desc = 'URI'
  reObj = url_regex

  def displayValue(self,valueindex=0,commandbutton=0):
    attr_value = self._ls.uc_decode(self.attrValue)[0]
    try:
      url,label = attr_value.split(u' ',1)
    except ValueError:
      url,label = attr_value,attr_value
      display_url = u''
    else:
      display_url = u' (%s)' % (url)
    if ldapurl.isLDAPUrl(url):
      return '<a href="%s?%s">%s%s</a>' % (
        self._form.script_name,
        self._form.utf2display(url),
        self._form.utf2display(label),
        self._form.utf2display(display_url),
      )
    else:
      return '<a href="%s/urlredirect/%s?%s">%s%s</a>' % (
        self._form.script_name,
        self._sid,
        self._form.utf2display(url),
        self._form.utf2display(label),
        self._form.utf2display(display_url),
      )


class Image(Binary):
  oid = 'Image-OID'
  desc = 'Image base class'
  mimeType = 'application/octet-stream'
  fileExt = 'bin'
  imageFormat = None

  def _validate(self,attrValue):
    return imghdr.what(None,attrValue)==self.imageFormat.lower()

  def sanitizeInput(self,attrValue):
    if not self._validate(attrValue) and PILImage:
      f = StringIO(attrValue)
      f2 = StringIO()
      try:
        try:
          im = PILImage.open(f)
          im.save(f2,self.imageFormat)
        except (IOError,ValueError),e:
          attrValue = None
        else:
          attrValue = f2.getvalue()
      finally:
        f.close()
    return attrValue

  def displayValue(self,valueindex=0,commandbutton=0):
    maxwidth,maxheight = 100,150
    width,height = None,None
    size_attr_html = ''
    if PILImage:
      f = StringIO(self.attrValue)
      try:
        im = PILImage.open(f)
      except IOError:
        pass
      else:
        width,height = im.size
        if width>maxwidth:
          size_attr_html = 'width="%d" height="%d"' % (maxwidth,int(float(maxwidth)/width*height))
        elif height>maxheight:
          size_attr_html = 'width="%d" height="%d"' % (int(float(maxheight)/height*width),maxheight)
        else:
          size_attr_html = 'width="%d" height="%d"' % (width,height)
    attr_value_len = len(self.attrValue)
    img_link = "%s/read/%s?dn=%s&amp;read_attr=%s&amp;read_attrindex=%d&amp;read_attrmode=load&amp;read_attrmimetype=image/jpeg" % (
      self._form.script_name,self._sid,
      urllib.quote(self._dn.encode(self._form.accept_charset)),
      urllib.quote(self.attrType),
      valueindex,
    )
    if attr_value_len<=0:
      return """
        <a href="%s">
          <img
            src="data:%s;base64,%s"
            alt="%d bytes of image data">
        </a>
        """ % (img_link,self.mimeType,self.attrValue.encode('base64'),attr_value_len)
    else:
      return """
        <a href="%s">
          <img
            src="%s"
            alt="%d bytes of image data" %s>
        </a>
        """ % (
        img_link,img_link,attr_value_len,size_attr_html,
      )


class JPEGImage(Image):
  oid = '1.3.6.1.4.1.1466.115.121.1.28'
  desc = 'JPEG image'
  mimeType = 'image/jpeg'
  fileExt = 'jpg'
  imageFormat = 'JPEG'


class PhotoG3Fax(Binary):
  oid = '1.3.6.1.4.1.1466.115.121.1.23'
  desc = 'Photo (G3 fax)'
  mimeType = 'image/g3fax'
  fileExt = 'tif'


class OID(IA5String):
  oid = '1.3.6.1.4.1.1466.115.121.1.38'
  desc = 'OID'
  reObj=re.compile(r'^([a-zA-Z]+[a-zA-Z0-9;-]*|[0-2]?\.([0-9]+\.)*[0-9]+)$')

  def displayValue(self,valueindex=0,commandbutton=0):
    try:
      name,description,reference = oid_desc_reg[self.attrValue]
    except (KeyError,ValueError):

      se = self._schema.get_obj(ldap.schema.ObjectClass,self.attrValue,None)
      if se is None:
        se = self._schema.get_obj(ldap.schema.AttributeType,self.attrValue,None)
        if se is None:
          return IA5String.displayValue(self,valueindex,commandbutton)
        else:
          return w2lapp.gui.SchemaElementName(
            self._sid,self._form,self._dn,self._schema,self.attrValue,ldap.schema.AttributeType,name_template=r'%s'
          )
      else:
        name_template = {
          0:r'%s <em>STRUCTURAL</em>',
          1:r'%s <em>ABSTRACT</em>',
          2:r'%s <em>AUXILIARY</em>'
        }[se.kind]
      # objectClass attribute is displayed with different function
      return w2lapp.gui.SchemaElementName(
        self._sid,self._form,self._dn,self._schema,self.attrValue,ldap.schema.ObjectClass,
        name_template=name_template
      )
      return IA5String.displayValue(self,valueindex,commandbutton)

    else:
      return '<strong>%s</strong> (%s):<br>%s (see %s)' % (
        self._form.utf2display(name),
        IA5String.displayValue(self,valueindex,commandbutton),
        self._form.utf2display(description),
        self._form.utf2display(reference)
      )


class LDAPUrl(DirectoryString):
  oid = 'LDAPUrl-oid'
  desc = 'LDAP URL'

  def displayValue(self,valueindex=0,commandbutton=0):
    try:
      if commandbutton:
        commandbuttonstr = w2lapp.gui.LDAPURLButton(self._sid,self._form,self._ls,self.attrValue)
      else:
        commandbuttonstr = ''
      return '<table><tr><td>%s</td><td><a href="%s">%s</a></td></tr></table>' % (
               commandbuttonstr,
               self._form.utf2display(self._ls.uc_decode(self.attrValue)[0]),
               self._form.utf2display(self._ls.uc_decode(self.attrValue)[0])
             )
    except ValueError:
      return '<strong>Not a valid LDAP URL:</strong> %s' % self._form.utf2display(repr(self.attrValue))


class OctetString(Binary):
  oid = '1.3.6.1.4.1.1466.115.121.1.40'
  desc = 'Octet String'
  editable = 1
  minInputRows = 1  # minimum number of rows for input field
  maxInputRows = 15 # maximum number of rows for in input field

  def sanitizeInput(self,attrValue):
    for c in [':',' ',',','\r','\n']:
      attrValue = attrValue.replace(c,'')
    result = []
    try:
      for i in range(0,len(attrValue),2):
        result.append(chr(int(attrValue[i:i+2],16)))
    except ValueError:
      raise LDAPSyntaxValueError, 'Illegal character in human-readable OctetString representation'
    else:
      result_str = ''.join(result)
    return result_str

  def displayValue(self,valueindex=0,commandbutton=0):
    return '\n<code>\n%s\n</code>\n' % mspki.util.HexString(
      self.attrValue,
      delimiter=':',wrap=64,linesep='<br>\n'
    )[:-1]

  def formValue(self):
    return unicode(mspki.util.HexString(self.attrValue or '',delimiter=':',wrap=48,linesep='\r\n'),'ascii')

  def formField(self):
    form_value = self.formValue()
    return pyweblib.forms.Textarea(
      self.attrType,
      ': '.join([self.attrType,self.desc]),
      10000,1,
      None,
      default=form_value,
      rows=max(self.minInputRows,min(self.maxInputRows,form_value.count('\r\n'))),
      cols=49
    )


class MultilineText(DirectoryString):
  oid = 'MultilineText-oid'
  desc = 'Multiple lines of text'
  reObj=re.compile('^.*$',re.S+re.M)
  lineSep = u'\r\n'
  mimeType = 'text/plain'
  cols = 66
  minInputRows = 1  # minimum number of rows for input field
  maxInputRows = 30 # maximum number of rows for in input field

  def _split_lines(self,v):
    return v.split(self.lineSep)

  def sanitizeInput(self,inputValue):
    return inputValue.replace(u'\r',u'').replace(u'\n',self.lineSep).encode(self._ls.charset)

  def displayValue(self,valueindex=0,commandbutton=0):
    lines = [
      self._form.utf2display(l)
      for l in self._split_lines(self._ls.uc_decode(self.attrValue)[0])
    ]
    return '<br>'.join(lines)

  def formValue(self):
    splitted_lines = self._split_lines(self._ls.uc_decode(self.attrValue or '')[0])
    return u'\r\n'.join(splitted_lines)

  def formField(self):
    form_value=self.formValue()
    return pyweblib.forms.Textarea(
      self.attrType,
      ': '.join([self.attrType,self.desc]),
      self.maxLen,self.maxValues,
      None,
      default=form_value,
      rows=max(self.minInputRows,min(self.maxInputRows,form_value.count('\r\n'))),
      cols=self.cols
    )


class PreformattedMultilineText(MultilineText):
  oid = 'PreformattedMultilineText-oid'
  lineSep = '\n'
  cols = 66

  def displayValue(self,valueindex=0,commandbutton=0):
    lines = [
      self._form.utf2display(l)
      for l in self._split_lines(self._ls.uc_decode(self.attrValue)[0])
    ]
    return '<code>%s</code>' % '<br>'.join(lines)


class PostalAddress(MultilineText):
  oid = '1.3.6.1.4.1.1466.115.121.1.41'
  desc = 'Postal Address'
  lineSep = ' $ '
  cols = 40

  def _split_lines(self,value):
    return [ v.strip() for v in value.split(self.lineSep.strip()) ]

  def sanitizeInput(self,inputValue):
    return inputValue.replace('\r','').replace('\n',self.lineSep)


class PrintableString(DirectoryString):
  oid = '1.3.6.1.4.1.1466.115.121.1.44'
  desc = 'Printable String'
  reObj= re.compile("^[a-zA-Z0-9'()+,.=/:? -]*$")
  charset = 'ascii'

class NumericString(PrintableString):
  oid = '1.3.6.1.4.1.1466.115.121.1.36'
  desc = 'Numeric String'
  reObj= re.compile('^[ 0-9]+$')


class EnhancedGuide(PrintableString):
  oid = '1.3.6.1.4.1.1466.115.121.1.21'
  desc = 'Enhanced Search Guide'


class Guide(EnhancedGuide):
  oid = '1.3.6.1.4.1.1466.115.121.1.25'
  desc = 'Search Guide'


class TelephoneNumber(PrintableString):
  oid = '1.3.6.1.4.1.1466.115.121.1.50'
  desc = 'Telephone Number'
  reObj= re.compile('^[0-9+x(). /-]+$')


class FacsimileTelephoneNumber(TelephoneNumber):
  oid = '1.3.6.1.4.1.1466.115.121.1.22'
  desc = 'Facsimile Number'
  reObj= re.compile('^[0-9+x(). /-]+(\$(twoDimensional|fineResolution|unlimitedLength|b4Length|a3Width|b4Width|uncompressed))*$')


class TelexNumber(PrintableString):
  oid = '1.3.6.1.4.1.1466.115.121.1.52'
  desc = 'Telex Number'
  reObj= re.compile("^[a-zA-Z0-9'()+,.=/:?$ -]*$")

class TeletexTerminalIdentifier(PrintableString):
  oid = '1.3.6.1.4.1.1466.115.121.1.51'
  desc = 'Teletex Terminal Identifier'


class ObjectGUID(LDAPSyntax):
  oid = 'ObjectGUID-oid'
  desc = 'Object GUID'
  charset = 'ascii'

  def displayValue(self,valueindex=0,commandbutton=0):
    objectguid_str = ''.join(['%02X' % ord(c) for c in self.attrValue])
    return ldapurl.LDAPUrl(
      ldapUrl=self._ls.uri,
      dn='GUID=%s' % (objectguid_str),
      who=None,cred=None
    ).htmlHREF(
      hrefText=objectguid_str,
      hrefTarget=None
    )


class Date(IA5String):
  oid = 'Date-oid'
  desc = 'Date in syntax specified by class attribute storageFormat'
  maxLen = 10
  storageFormat = '%Y-%m-%d'
  acceptableDateformats = (
    '%Y-%m-%d',
    '%d.%m.%Y',
    '%m/%d/%Y',
  )

  def _validate(self,attrValue):
    try:
      datetime.datetime.strptime(attrValue,self.storageFormat)
    except ValueError:
      return 0
    else:
      return 1

  def sanitizeInput(self,attrValue):
    attrValue = attrValue.strip()
    for time_format in self.acceptableDateformats:
      try:
        time_tuple = datetime.datetime.strptime(attrValue,time_format)
      except ValueError:
        result = attrValue
      else:
        result = datetime.datetime.strftime(time_tuple,self.storageFormat)
        break
    return result # sanitizeInput()


class NumstringDate(Date):
  oid = 'NumstringDate-oid'
  desc = 'Date in syntax YYYYMMDD'
  reObj = re.compile('^[0-9]{4}[0-1][0-9][0-3][0-9]$')
  storageFormat = '%Y%m%d'


class ISO8601Date(Date):
  oid = 'ISO8601Date-oid'
  desc = 'Date in syntax YYYY-MM-DD, see ISO 8601'
  reObj = re.compile('^[0-9]{4}-[0-1][0-9]-[0-3][0-9]$')
  storageFormat = '%Y-%m-%d'


class SecondsSinceEpoch(Integer):
  oid = 'SecondsSinceEpoch-oid'
  desc = 'Seconds since epoch (1970-01-01 00:00:00)'

  def displayValue(self,valueindex=0,commandbutton=0):
    try:
      return utctime.strftimeiso8601(time.gmtime(float(self.attrValue))).encode('ascii')
    except ValueError:
      return Integer.displayValue(self,valueindex,commandbutton)


class DaysSinceEpoch(Integer):
  oid = 'DaysSinceEpoch-oid'
  desc = 'Days since epoch (1970-01-01)'

  def displayValue(self,valueindex=0,commandbutton=0):
    try:
      return utctime.strftimeiso8601(time.gmtime(float(self.attrValue)*86400)).encode('ascii')
    except ValueError:
      return Integer.displayValue(self,valueindex,commandbutton)


class Timespan(Integer):
  oid = 'Timespan-oid'
  desc = 'Time span in seconds'
  time_divisors = (
    (u'weeks',604800),
    (u'days',86400),
    (u'hours',3600),
    (u'mins',60),
    (u'secs',1),
  )
  sep = u','

  def sanitizeInput(self,attrValue):
    if attrValue:
      try:
        result = str(w2lapp.gui.repr2ts(self.time_divisors,self.sep,attrValue))
      except ValueError:
        result = Integer.sanitizeInput(self,attrValue)
    else:
      result = attrValue
    return result

  def formValue(self):
    if not self.attrValue:
      return self.attrValue
    try:
      result = w2lapp.gui.ts2repr(self.time_divisors,self.sep,int(self.attrValue))
    except ValueError:
      result = Integer.formValue(self)
    return result

  def displayValue(self,valueindex=0,commandbutton=0):
    try:
      result = self._form.utf2display('%s (%s)' % (
        w2lapp.gui.ts2repr(self.time_divisors,self.sep,int(self.attrValue)),
        Integer.displayValue(self,valueindex,commandbutton)
      ))
    except ValueError:
      result = Integer.displayValue(self,valueindex,commandbutton)
    return result


class SelectList(DirectoryString):
  """
  Base class for dictionary based select lists which
  should not be used directly
  """
  oid = 'SelectList-oid'
  attr_value_dict = {} # Mapping attribute value to attribute description

  def _sortedSelectOptions(self):
    current_attr_values = set([self._ls.uc_decode(v)[0] for v in self._entry.get(self.attrType,[])])
    attr_value_u = DirectoryString.formValue(self)
    current_attr_value_dict = {}
    current_attr_value_dict.update(self.attr_value_dict)
    if not current_attr_value_dict.has_key(u''):
      current_attr_value_dict[u''] = u'- not set -'
    if not attr_value_u in current_attr_value_dict:
      current_attr_value_dict[attr_value_u] = attr_value_u
    tmplist = [
      (x[1], x)
      for x in current_attr_value_dict.items()
      if x[0]==attr_value_u or not x[0] in current_attr_values
    ]
    tmplist.sort()
    return [ x for (k,x) in tmplist ]

  def _validate(self,attrValue):
    return self.attr_value_dict.has_key(unicode(attrValue,self._form.accept_charset))

  def displayValue(self,valueindex=0,commandbutton=0):
    attr_value_str = self._form.utf2display(self._ls.uc_decode(self.attrValue)[0])
    try:
      attr_value_desc=self.attr_value_dict[self.attrValue]
    except KeyError:
      return attr_value_str
    else:
      if attr_value_desc==self.attrValue:
        return attr_value_str
      else:
        return ': '.join((
          self._form.utf2display(attr_value_desc),attr_value_str
        ))

  def formField(self):
    if not self.attr_value_dict or not filter(None,self.attr_value_dict.keys()):
      return DirectoryString.formField(self)
    else:
      f = pyweblib.forms.Select(
        self.attrType,
        ': '.join([self.attrType,self.desc]),1,
        options=self._sortedSelectOptions(),
        default=self.formValue(),
        required=0
      )
      f.charset=self._form.accept_charset
      return f


class PropertiesSelectList(SelectList):
  oid = 'PropertiesSelectList-oid'
  properties_pathname = None
  properties_charset = 'utf-8'
  properties_delimiter = u'='

  def __init__(self,sid,form,ls,dn,schema,attrType,attrValue,entry=None):
    SelectList.__init__(self,sid,form,ls,dn,schema,attrType,attrValue,entry)
    self._readProperties()

  def _readProperties(self):
    real_path_name = w2lapp.gui.GetVariantFilename(
      self.properties_pathname,
      self._form.accept_language
    )
    f = open(real_path_name,'rb')
    self.attr_value_dict = {}
    for line in f.readlines():
      line = line.decode(self.properties_charset).strip()
      if line and not line.startswith('#'):
        key,value = line.split(self.properties_delimiter)
        self.attr_value_dict[key.strip()] = value.strip()
    return # _readProperties()


class DynamicValueSelectList(SelectList,DirectoryString):
  oid = 'DynamicValueSelectList-oid'
  ldap_url = None
  valuePrefix = ''
  valueSuffix = ''

  def __init__(self,sid,form,ls,dn,schema,attrType,attrValue,entry=None):
    self.lu_obj = ldapurl.LDAPUrl(self.ldap_url)
    self.minLen = len(self.valuePrefix)+len(self.valueSuffix)
    SelectList.__init__(self,sid,form,ls,dn,schema,attrType,attrValue,entry)

  def formField(self):
    self._doSearch()
    return SelectList.formField(self)

  def _searchReferencedEntry(self,attrValue):
    search_dn = self._determineSearchDN(self._dn,self.lu_obj.dn)
    try:
      ldap_result = self._ls.l.search_ext_s(
        self._ls.uc_encode(search_dn)[0],
        self.lu_obj.scope,
        '(&%s(%s=%s))' % (
          self.lu_obj.filterstr or '(objectClass=*)',
          self.lu_obj.attrs[0],attrValue
        ),
        attrlist=self.lu_obj.attrs,
        sizelimit=2,
      )
    except (
      ldap.NO_SUCH_OBJECT,
      ldap.CONSTRAINT_VIOLATION,
      ldap.INSUFFICIENT_ACCESS,
    ),e:
      return None
    else:
      # Filter out LDAP referrals
      ldap_result = [
        (dn,entry)
        for dn,entry in ldap_result
        if dn!=None
      ]
      if ldap_result and len(ldap_result)==1:
        return ldap_result[0]
      else:
        return None

  def _validate(self,attrValue):
    if not attrValue.startswith(self.valuePrefix) or \
       not attrValue.endswith(self.valueSuffix) or \
       len(attrValue)<self.minLen:
      return 0
    inner_attr_value = attrValue[len(self.valuePrefix):len(attrValue)-len(self.valueSuffix)]
    return self._searchReferencedEntry(inner_attr_value)!=None

  def displayValue(self,valueindex=0,commandbutton=0):
    if commandbutton and self.lu_obj.attrs:
      ref_result = self._searchReferencedEntry(self.attrValue)
      if ref_result:
        ref_dn,ref_entry = ref_result
        try:
          attr_value_desc=self._ls.uc_decode(ref_entry[self.lu_obj.attrs[1]][0])[0]
        except (KeyError,IndexError),e:
          display_text,link_html = '',''
        else:
          if self.lu_obj.attrs[0].lower()==self.lu_obj.attrs[1].lower():
            display_text = ''
          else:
            display_text = self._form.utf2display(attr_value_desc+u':')
          if commandbutton:
            link_html = self._form.applAnchor(
              'read','&raquo;',self._sid,
              [('dn',self._ls.uc_decode(ref_dn)[0])],
            )
          else:
            link_html = ''
      else:
        display_text,link_html = '',''
    else:
      display_text,link_html = '',''
    return ' '.join((
      display_text,
      DirectoryString.displayValue(self,valueindex,commandbutton),
      link_html,
    ))

  def _determineSearchDN(self,current_dn,ldap_url_dn):
    ldap_url_dn = self._ls.uc_decode(ldap_url_dn)[0]
    if ldap_url_dn=='_':
      result_dn = self._ls.getSearchRoot(self._dn)
    elif ldap_url_dn=='.':
      result_dn = current_dn
    elif ldap_url_dn=='..':
      result_dn = ldaputil.base.ParentDN(current_dn)
    elif ldap_url_dn.endswith(',_'):
      result_dn = ','.join((ldap_url_dn[:-2],self._ls.getSearchRoot(self._dn)))
    elif ldap_url_dn.endswith(',.'):
      result_dn = ','.join((ldap_url_dn[:-2],current_dn))
    elif ldap_url_dn.endswith(',..'):
      result_dn = ','.join((ldap_url_dn[:-3],ldaputil.base.ParentDN(current_dn)))
    else:
      result_dn = ldap_url_dn
    if result_dn.endswith(','):
      result_dn = result_dn[:-1]
    return result_dn # _determineSearchDN()

  def _doSearch(self,initial_attr_value_dict=None):
    # Enable empty value in any case
    self.attr_value_dict = initial_attr_value_dict or {}
    if self.lu_obj.hostport:
      # New connection to separate server
      # not implemented yet!
      pass
    else:
      search_dn = self._determineSearchDN(self._dn,self.lu_obj.dn)
      search_scope = self.lu_obj.scope or ldap.SCOPE_BASE
      # Use the existing LDAP connection as current user
      try:
        ldap_result = self._ls.l.search_s(
          self._ls.uc_encode(search_dn)[0],
          search_scope,
          self.lu_obj.filterstr or '(objectClass=*)',
          self.lu_obj.attrs or ['objectClass']
        )
      except (
        ldap.NO_SUCH_OBJECT,
        ldap.SIZELIMIT_EXCEEDED,
        ldap.TIMELIMIT_EXCEEDED,
        ldap.PARTIAL_RESULTS,
        ldap.INSUFFICIENT_ACCESS,
        ldap.CONSTRAINT_VIOLATION,
      ):
        self.attr_value_dict = {}
        return
    if search_scope==ldap.SCOPE_BASE:
      dn_r,entry_r=ldap_result[0]
      # When reading a single entry we build the map from a single multi-valued attribute
      assert len(self.lu_obj.attrs or [])==1,"attrlist in ldap_url must be of length 1 if scope is base"
      list_attr = self.lu_obj.attrs[0]
      attr_values_u = [
        ''.join((
          self.valuePrefix,
          self._ls.uc_decode(attr_value)[0],
          self.valueSuffix,
        ))
        for attr_value in entry_r[list_attr]
      ]
      self.attr_value_dict=dict([ (u,u) for u in attr_values_u ])
    else:
      try:
        option_value_map,option_text_map = (self.lu_obj.attrs or [])[:2]
      except ValueError:
        option_value_map,option_text_map = (None,(self.lu_obj.attrs or [None])[0])
      for dn_r,entry_r in ldap_result:
        # Check whether it's a real search result (ignore search continuations)
        if not dn_r is None:
          entry_r[None] = [dn_r]
          try:
            self.attr_value_dict[
              ''.join((
                self.valuePrefix,
                self._ls.uc_decode(entry_r[option_value_map][0])[0],
                self.valueSuffix,
              ))
            ] = self._ls.uc_decode(entry_r.get(option_text_map,entry_r[option_value_map])[0])[0]
          except KeyError:
            pass
    return # _doSearch()


class DynamicDNSelectList(DynamicValueSelectList,DistinguishedName):
  oid = 'DynamicDNSelectList-oid'

  def _readReferencedEntry(self,dn):
    try:
      ldap_result = self._ls.readEntry(
        dn.decode(self._ls.charset),
        attrtype_list=self.lu_obj.attrs,
        search_filter=self.lu_obj.filterstr or '(objectClass=*)',
      )
    except (
      ldap.NO_SUCH_OBJECT,
      ldap.CONSTRAINT_VIOLATION,
      ldap.INSUFFICIENT_ACCESS,
    ),e:
      return None
    else:
      if ldap_result:
        return ldap_result[0][1]
      else:
        return None

  def _validate(self,attrValue):
    return self._readReferencedEntry(attrValue)!=None

  def displayValue(self,valueindex=0,commandbutton=0):
    if commandbutton and self.lu_obj.attrs:
      ref_entry = self._readReferencedEntry(self.attrValue) or {}
      try:
        attr_value_desc=self._ls.uc_decode(ref_entry[self.lu_obj.attrs[0]][0])[0]
      except (KeyError,IndexError),e:
        display_text = ''
      else:
        display_text = self._form.utf2display(attr_value_desc+u': ')
    else:
      display_text = ''
    return ''.join((
      display_text,
      DistinguishedName.displayValue(self,valueindex,commandbutton)
    ))


class Boolean(SelectList,IA5String):
  oid = '1.3.6.1.4.1.1466.115.121.1.7'
  desc = 'Boolean'
  attr_value_dict = {
    u'TRUE':u'TRUE',
    u'FALSE':u'FALSE',
  }

  def _sortedSelectOptions(self):
    result = SelectList._sortedSelectOptions(self)
    if self.attrValue and self.attrValue.lower()==self.attrValue:
      return [ (o.lower(),t) for o,t in result ]
    else:
      return result

  def _validate(self,attrValue):
    return attrValue.upper() in self.attr_value_dict

  def displayValue(self,valueindex=0,commandbutton=0):
    return IA5String.displayValue(self,valueindex,commandbutton)


class EnabledFlag(Boolean):
  oid = 'EnabledFlag-oid'
  desc = 'TRUE means enabled, FALSE means disabled'
  attr_value_dict = {
    u'TRUE':u'enabled',
    u'FALSE':u'disabled',
  }


class CountryString(SelectList):
  oid = '1.3.6.1.4.1.1466.115.121.1.11'
  desc = 'Two letter country string as listed in ISO 3166-2'
  attr_value_dict = w2lapp.cnf.countries.c_dict

  def sanitizeInput(self,attrValue):
    return attrValue.upper().strip()


class DeliveryMethod(PrintableString):
  oid = '1.3.6.1.4.1.1466.115.121.1.14'
  desc = 'Delivery Method'
  pdm = '(any|mhs|physical|telex|teletex|g3fax|g4fax|ia5|videotex|telephone)'
  reObj= re.compile('^%s[ $]*%s$' % (pdm,pdm))


class BitArrayInteger(MultilineText,Integer):
  oid = 'BitArrayInteger-oid'
  flag_desc_table = ()
  true_false_desc={1:'+',0:'-'}
  minValue=0

  def __init__(self,sid,form,ls,dn,schema,attrType,attrValue,entry=None):
    Integer.__init__(self,sid,form,ls,dn,schema,attrType,attrValue)
    self.flag_desc2int=dict(self.flag_desc_table)
    self.flag_int2desc=dict([(j,i) for i,j in self.flag_desc_table])
    self.maxValue=sum([j for i,j in self.flag_desc_table])
    self.minInputRows=maxInputRows=max(len(self.flag_desc_table),1)

  def sanitizeInput(self,inputValue):
    result = 0
    for row in inputValue.split('\n'):
      row=row.strip()
      try:
        flag_set,flag_desc=row[0],row[1:]
      except IndexError:
        pass
      else:
        if flag_set=='+':
          try:
            result=result|self.flag_desc2int[flag_desc]
          except KeyError:
            pass
    return str(result)

  def formValue(self):
    attr_value_int=int(self.attrValue or 0)
    flag_lines = [
      ''.join((
        self.true_false_desc[int((attr_value_int&flag_int)>0)],
        flag_desc
      ))
      for flag_desc,flag_int in self.flag_desc_table
    ]
    return u'\r\n'.join(flag_lines)

  def formField(self):
    form_value=self.formValue()
    return pyweblib.forms.Textarea(
      self.attrType,
      ': '.join([self.attrType,self.desc]),
      self.maxLen,self.maxValues,
      None,
      default=form_value,
      rows=max(self.minInputRows,min(self.maxInputRows,form_value.count('\n'))),
      cols=max([len(desc) for desc,value in self.flag_desc_table])+1
    )

  def displayValue(self,valueindex=0,commandbutton=0):
    attrValue_int = int(self.attrValue)
    return """%s<br>
    <table summary="Flags">
    <tr><th>Property flag</th><th>Value</th><th>Status</th></tr>
    %s
    </table>
    """ % (
      Integer.displayValue(self,valueindex,commandbutton),
      '\n'.join([
        '<tr><td>%s</td><td>%s</td><td>%s</td></tr>' % (
          desc,
          hex(flag_value),
          {0:'-',1:'on'}[int((attrValue_int & flag_value)>0)]
        )
        for desc,flag_value in self.flag_desc_table
      ])
    )


class GSER(DirectoryString):
  oid = 'GSER-oid'
  desc = 'GSER syntax (see RFC 3641)'


class UUID(IA5String):
  oid = '1.3.6.1.1.16.1'
  desc = 'UUID'
  reObj = re.compile('^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$')

  def sanitizeInput(self,attrValue):
    try:
      return str(uuid.UUID(attrValue.replace(':','')))
    except ValueError:
      return attrValue


class DNSDomain(IA5String):
  oid = 'DNSDomain-oid'
  desc = 'DNS domain name (see RFC 1035)'
  reObj = re.compile('^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$')

  def sanitizeInput(self,attrValue):
    return '.'.join([
      dc.lower().encode('idna')
      for dc in attrValue.decode(self._form.accept_charset).split(u'.')
    ])

  def formValue(self):
    try:
      result = u'.'.join([
        dc.decode('idna')
        for dc in (self.attrValue or '').split('.')
      ])
    except UnicodeDecodeError:
      result = u'!!!snipped because of UnicodeDecodeError!!!'
    return result

  def displayValue(self,valueindex=0,commandbutton=0):
    if self.attrValue.decode('ascii')!=self.attrValue.decode('idna'):
      return '%s (%s)' % (
        IA5String.displayValue(self,valueindex,commandbutton),
        self._form.utf2display(self.formValue())
      )
    else:
      return IA5String.displayValue(self,valueindex,commandbutton)


class DomainComponent(DNSDomain):
  oid = 'DomainComponent-oid'
  desc = 'DNS domain name component'
  reObj = re.compile('^[a-zA-Z0-9-]+$')


class YesNoIntegerFlag(SelectList):
  oid = 'YesNoIntegerFlag-oid'
  desc = '0 means no, 1 means yes'
  attr_value_dict = {
    u'0':u'no',
    u'1':u'yes',
  }


class OnOffFlag(SelectList):
  oid = 'OnOffFlag-oid'
  desc = 'Only values "on" or "off" are allowed'
  attr_value_dict = {
    u'on':u'on',
    u'off':u'off',
  }


try:
  # Python 2.7
  from xml.etree.ElementTree import ParseError as XMLParseError
except ImportError:
  # Python 2.6
  from xml.parsers.expat import ExpatError as XMLParseError

class XmlValue(MultilineText):
  oid = 'XmlValue-oid'
  desc = 'XML data'
  lineSep = '\n'
  mimeType = 'text/xml'
  cols = 64
  whitespace_cleaning = False

  def _validate(self,attrValue):
    try:
      xml.etree.ElementTree.XML(attrValue)
    except XMLParseError,e:
      return False
    else:
      return True

  def sanitizeInput(self,attrValue):
    if self.whitespace_cleaning:
      return self.lineSep.join([
        l.rstrip()
        for l in self._split_lines(attrValue.decode(self._form.accept_charset))
      ]).encode(self._ls.charset)
    else:
      return attrValue

  def displayValue(self,valueindex=0,commandbutton=0):
    lines = [
      self._form.utf2display(l,tab_identiation='&nbsp;&nbsp;&nbsp;&nbsp;')
      for l in self._split_lines(self._ls.uc_decode(self.attrValue)[0])
    ]
    return '<code>%s</code>' % '<br>'.join(lines)


try:
  # Try to import optional module pisces
  from pisces import asn1

except ImportError:
  # Fall-back class is Binary
  ASN1Object = Binary

else:

  class ASN1Object(Binary):
    oid = 'ASN1Object-oid'
    desc = 'BER encoded ASN.1 data'

    def displayValue(self,valueindex=0,commandbutton=0):
      asn1obj = asn1.parse(self.attrValue)
      return ''.join((
        '<code>',
        self._form.utf2display(
          str(asn1obj).decode('utf-8').replace('{','\n{').replace('}','}\n')
        ).replace('  ','&nbsp;&nbsp;').replace('\n','<br>'),
        '</code>'
      ))

  class DumpASN1CfgOID(OID):
    oid = 'DumpASN1Cfg-oid'
    desc = "OID registered in Peter Gutmann's dumpasn1.cfg"

    def displayValue(self,valueindex=0,commandbutton=0):
      attrValue = self.attrValue.encode('ascii')
      try:
        pisces_oid = asn1.OID(tuple(map(int,attrValue.split('.'))))
        desc = mspki.asn1helper.GetOIDDescription(
          pisces_oid,
          mspki.asn1helper.oids,
          includeoid=1
        )
      except ValueError:
        return self._form.utf2display(self.attrValue)
      else:
        return desc


# Set up the central syntax registry instance
syntax_registry = SyntaxRegistry()
# Register all syntax classes in this module
for symbol_name in dir():
  syntax_registry.registerSyntaxClass(eval(symbol_name))
