# -*- coding: utf-8 -*-
"""
w2lapp.gui.py: basic functions for GUI elements

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 os,urllib,ldap,ldapurl, \
       ldaputil,pyweblib.forms,pyweblib.httphelper,msbase, \
       w2lapp.core,w2lapp.cnf,w2lapp.schema.syntaxes,w2lapp.searchform

from ldapurl import LDAPUrl,LDAPUrlExtension,isLDAPUrl
from ldap.filter import escape_filter_chars
from pyweblib.forms import escapeHTML

from ldaputil.base import \
  ParentDN,ParentDNList,explode_dn, \
  AD_LDAP49_ERROR_CODES,AD_LDAP49_ERROR_PREFIX

from msbase import GrabKeys

from types import UnicodeType

########################################################################
# Initialize some constants used throughout web2ldap
########################################################################

host_pattern = '[a-zA-Z0-9_.:\[\]-]+'

HIDDEN_FIELD = '<input type="hidden" name="%s" value="%s">%s\n'

# This function searches for variants
def GetVariantFilename(pathname,variantlist):
  checked_set = set()
  for v in variantlist:
    # Strip subtags
    v = v.lower().split('-',1)[0]
    if v=='en':
      variant_filename = pathname
    else:
      variant_filename = '.'.join((pathname,v))
    if not v in checked_set and os.path.isfile(variant_filename):
      break
    else:
      checked_set.add(v)
  else:
    variant_filename = pathname
  return variant_filename


def ReadTemplate(form,ls,config_key,form_desc=u'',tmpl_filename=None):
  if not tmpl_filename:
    tmpl_filename = w2lapp.cnf.GetParam(ls or '_',config_key,None)
  if not tmpl_filename:
    raise w2lapp.core.ErrorExit(u'No template specified for %s.' % (form_desc))
  tmpl_filename = w2lapp.gui.GetVariantFilename(tmpl_filename,form.accept_language)
  try:
    # Read template from file
    tmpl_str = open(tmpl_filename,'r').read()
  except IOError:
    raise w2lapp.core.ErrorExit(u'I/O error during reading %s template file.' % (form_desc))
  return tmpl_str # ReadTemplate()


def LDAPError2ErrMsg(e,form,charset='utf-8',template='{error_msg}<br>{matched_dn}'):
  """
  Converts a LDAPError exception into HTML error message

  e
    LDAPError instance
  form
    Web2LDAPForm instance
  charset
    Character set for decoding the LDAP error messages (diagnosticMessage)
  template
    Raw binary string to be used as template
    (must contain only a single placeholder)
  """

  matched_dn = None

  if isinstance(e,ldap.TIMEOUT) or not e.args:

    ErrMsg = u''

  elif isinstance(e,ldap.INVALID_CREDENTIALS) and \
       AD_LDAP49_ERROR_PREFIX in e.args[0].get('info',''):

    ad_error_code_pos = e.args[0]['info'].find(AD_LDAP49_ERROR_PREFIX)+len(AD_LDAP49_ERROR_PREFIX)
    ad_error_code = int(e.args[0]['info'][ad_error_code_pos:ad_error_code_pos+3],16)
    ErrMsg = u'%s:\n%s (%s)' % (
      unicode(e.args[0]['desc'],charset),
      unicode(e.args[0].get('info',''),charset),
      AD_LDAP49_ERROR_CODES.get(ad_error_code,u'unknown'),
    )

  else:

    try:
      ErrMsg = u':\n'.join((
        unicode(e.args[0]['desc'],charset),
        unicode(e.args[0].get('info',''),charset)
      ))
    except UnicodeDecodeError:
      ErrMsg = u':\n'.join((
        unicode(e.args[0]['desc'],charset),
        unicode(repr(e.args[0].get('info','')),charset)
      ))
    except TypeError:
      try:
        ErrMsg = u':\n'.join((
          unicode(e[0],charset),
          unicode(e[1],charset)
        ))
      except (TypeError,IndexError):
        ErrMsg = unicode(str(e),charset)
    else:
      try:
        matched_dn = unicode(e.args[0].get('matched',''),charset)
      except KeyError:
        matched_dn = None

  ErrMsg = ErrMsg.replace(u'\r','').replace(u'\t','')
  ErrMsg_html = form.utf2display(ErrMsg,lf_entity='<br>')

  # Add matchedDN to error message HTML if needed
  if matched_dn:
    matched_dn_html = '<br>Matched DN: %s' % (form.utf2display(matched_dn))
  else:
    matched_dn_html = ''

  return template.format(
    error_msg=ErrMsg_html,
    matched_dn=matched_dn_html
  )


def ts2repr(time_divisors,ts_sep,ts_value):
  rest = long(ts_value)
  result = []
  for desc,divisor in time_divisors:
    mult = rest / divisor
    rest = rest % divisor
    if mult>0:
      result.append(u'%d %s' % (mult,desc))
    if rest==0:
      break
  return ts_sep.join(result)


def repr2ts(time_divisors,ts_sep,value):
  l1 = [ v.strip().split(u' ') for v in value.split(ts_sep) ]
  l2 = [
    (int(v),d.strip())
    for v,d in l1
  ]
  time_divisors_dict = dict(time_divisors)
  result = 0
  for value,desc in l2:
    try:
      result += value*time_divisors_dict[desc]
    except KeyError:
      raise ValueError
    else:
      del time_divisors_dict[desc]
  return result


def DisplayDN(sid,form,ls,dn,commandbutton=0,active_ldapurl=0,bindas_button=False):
  """Display a DN as LDAP URL with or without button"""
  assert type(dn)==UnicodeType, "Argument 'dn' must be UnicodeType"
  dn_str = form.utf2display(dn or u'- World -')
  if active_ldapurl:
    href_str = ls.ldapUrl(dn).htmlHREF(hrefText=dn_str,hrefTarget=None)
  else:
    href_str = dn_str
  if commandbutton:
    command_buttons = [
      href_str,
      form.applAnchor('read','Read',sid,[('dn',dn)])
    ]
    if bindas_button:
      command_buttons.append(
        form.applAnchor('login','Bind as',sid,[('login_who',dn)])
      )
    return ' | '.join(command_buttons)
  else:
    return href_str


def CommandTable(
  outf,
  commandlist,
  div_id='CommandDiv',
  separator=' '
):
  if commandlist:
    outf.write('<nav><p id="%s" class="CommandTable">\n%s\n</p></nav>\n' % (
      div_id,
      (separator).join(commandlist)
    ))
  return # CommandTable()


def EntryMainMenu(form):
  return [
    form.applAnchor('','Connect',None,[]),
    form.applAnchor('monitor','Monitor',None,[]),
    form.applAnchor('locate','DNS lookup',None,[]),
  ]


def ContextMenuSingleEntry(sid,form,ls,dn,vcard_link=0,dds_link=0,entry_uuid=None):
  """
  Output the context menu for a single entry
  """

  result = [
    form.applAnchor('read','Raw',sid,[('dn',dn),('read_output','table'),('read_expandattr','*')],title=u'Display this entry as raw attribute type/value list'),
    form.applAnchor('login','Bind as',sid,[('dn',dn),('login_who',dn)],title=u"Login with this entry's DN as bind-DN"),
    form.applAnchor('modifyform','Modify',sid,[('dn',dn)],title=u'Modify this entry'),
    form.applAnchor('rename','Rename',sid,[('dn',dn)],title=u'Rename/move this entry'),
    form.applAnchor('delete','Delete',sid,[('dn',dn)],title=u'Delete this entry and/or subtree'),
    form.applAnchor('passwd','Password',sid,[('dn',dn),('passwd_who',dn)],title=u'Set user password for this entry'),
    form.applAnchor('groupadm','Groups',sid,[('dn',dn)],title=u'Change group membership for this entry'),
  ]

  if vcard_link:
    result.append(form.applAnchor('read','vCard',sid,[('dn',dn),('read_output','vcard')],title=u'Export this entry as vCard'))

  if dds_link:
    result.append(form.applAnchor('dds','Refresh',sid,[('dn',dn)],title=u'Refresh dynamic entry'))

  current_audit_context = ls.getAuditContext(ls.currentSearchRoot)
  if not current_audit_context is None:
    if entry_uuid:
      accesslog_any_filterstr = '(&(objectClass=auditObject)(|(reqDN=%s)(reqEntryUUID=%s)))' % (
        escape_filter_chars(dn),
        escape_filter_chars(entry_uuid),
      )
      accesslog_write_filterstr = '(&(objectClass=auditWriteObject)(|(reqDN=%s)(reqEntryUUID=%s)))' % (
        escape_filter_chars(dn),
        escape_filter_chars(entry_uuid),
      )
    else:
      accesslog_any_filterstr = '(&(objectClass=auditObject)(reqDN=%s))' % (
        escape_filter_chars(dn)
      )
      accesslog_write_filterstr = '(&(objectClass=auditWriteObject)(reqDN=%s))' % (
        escape_filter_chars(dn)
      )
    result.extend([
      form.applAnchor(
        'search','Audit access',sid,
        [
          ('dn',current_audit_context),
          ('filterstr',accesslog_any_filterstr),
          ('scope',str(ldap.SCOPE_ONELEVEL)),
        ],
        title=u'Complete audit trail for current entry',
      ),
      form.applAnchor(
        'search','Audit writes',sid,
        [
          ('dn',current_audit_context),
          ('filterstr',accesslog_write_filterstr),
          ('scope',str(ldap.SCOPE_ONELEVEL)),
        ],
        title=u'Audit trail of write access to current entry',
      ),
    ])

  if ls.rootDSE.has_key('changelog') and not dn.endswith(ls.rootDSE['changelog'][0]):
    if entry_uuid:
      changelog_filterstr = '(&(objectClass=changeLogEntry)(|(targetDN=%s)(targetEntryUUID=%s)))' % (
        escape_filter_chars(dn),
        escape_filter_chars(entry_uuid),
      )
    else:
      changelog_filterstr = '(&(objectClass=changeLogEntry)(targetDN=%s))' % (
        escape_filter_chars(dn)
      )
    result.append(
      form.applAnchor(
        'search','Change log',sid,
        [
          ('dn',ls.rootDSE['changelog'][0]),
          ('filterstr',changelog_filterstr),
          ('scope',str(ldap.SCOPE_ONELEVEL)),
        ],
        title=u'Audit trail of write access to current entry',
      )
    )

  try:
    monitor_context_dn = ls.rootDSE['monitorContext'][0]
  except KeyError:
    pass
  else:
    result.append(form.applAnchor(
      'search','User conns',sid,
      [
        ('dn',monitor_context_dn),
        ('filterstr','(&(objectClass=monitorConnection)(monitorConnectionAuthzDN=%s))' % (escape_filter_chars(dn))),
        ('scope',str(ldap.SCOPE_SUBTREE)),
      ],
      title=u'Find connections of this user in monitor database',
    ))

  return result # ContextMenuSingleEntry()


def WhoAmITemplate(sid,outf,form,ls,dn,who=None,entry=None):
  if who==None:
    if hasattr(ls,'who') and ls.who:
      who = ls.who
      entry = ls.userEntry
    else:
      return 'anonymous'
  # Determine relevant templates dict
  bound_as_templates = ldap.cidict.cidict(w2lapp.cnf.GetParam(
    ls.ldapUrl(ls.getSearchRoot(who)),'boundas_template',{}
  ))
  # Read entry if necessary
  if entry==None:
    read_attrs = set(['objectClass'])
    for oc in bound_as_templates.keys():
      read_attrs.update(GrabKeys(bound_as_templates[oc]).keys)
    try:
      ldap_result = ls.readEntry(who,attrtype_list=list(read_attrs))
    except ldap.LDAPError,e:
      entry = {}
    else:
      if ldap_result:
        _,entry = ldap_result[0]
      else:
        entry = {}
  if ldaputil.base.is_dn(who):
    if entry:
      sub_schema = ls.retrieveSubSchema(who,w2lapp.cnf.GetParam(ls,'_schema',None))
      user_entry_display_dict = msbase.CaseinsensitiveStringKeyDict(
        default_dict=dict([
          (
            attr_type.lower(),
            w2lapp.gui.DataStr(sid,form,ls,dn,sub_schema,attr_type,attr_values[0],commandbutton=0)
          )
          for attr_type,attr_values in entry.items()
        ]),
        default='',
      )
      try:
        user_object_classes = [ oc.lower() for oc in entry['objectClass'] ]
      except KeyError:
        result = DisplayDN(sid,form,ls,who,commandbutton=0,active_ldapurl=0,bindas_button=False)
      else:
        bound_as_templates = ldap.cidict.cidict(w2lapp.cnf.GetParam(
          ls.ldapUrl(ls.getSearchRoot(who)),'boundas_template',{}
        ))
        for oc in user_object_classes:
          try:
            result = bound_as_templates[oc] % user_entry_display_dict
          except KeyError:
            pass
          else:
            break
        else:
          result = DisplayDN(sid,form,ls,who,commandbutton=0,active_ldapurl=0,bindas_button=False)
    else:
      result = DisplayDN(sid,form,ls,who,commandbutton=0,active_ldapurl=0,bindas_button=False)
  else:
    result = form.utf2display(who)
  return result # WhoAmITemplate()


def StatusLine(sid,outf,form,ls,dn,who_templates=None):
  """
  Status line displaying host, current DN, LDAP URL and bind DN
  """

  status_template_str = w2lapp.gui.ReadTemplate(form,ls,'status_template',u'status section')

  template_dict = {
    'sid':sid or '',
    'SCRIPT_NAME':escapeHTML(form.script_name),
    'web2ldap_version':escapeHTML(w2lapp.__version__),
    'host':'not connected',
    'description':'',
    'who':'',
    'dit_navi':'',
    '':'',
  }
  template_dict.update([(k,escapeHTML(str(v))) for k,v in form.env.items()])
  if ls!=None and ls.uri!=None:
    # Only output something meaningful if valid connection
    template_dict.update({
      'host':ls.ldapUrl(dn).htmlHREF(
        hrefText=(ls.uri or u'disconnected host').encode(form.accept_charset),
        hrefTarget=None
      ),
#      'dn':DisplayDN(sid,form,ls,dn,active_ldapurl=1),
      'description':escapeHTML(w2lapp.cnf.GetParam(ls,'description',u'').encode(form.accept_charset)),
      'dit_navi':','.join(DITNavigationList(sid,outf,form,ls,dn,level=0)),
    })
    template_dict['who'] =  WhoAmITemplate(sid,outf,form,ls,dn)

  outf.write(status_template_str.format(**template_dict))


def MainMenu(sid,form,ls,dn):
  """
  Returns list of main menu items
  """
  cl = []

  if ls!=None and ls.uri!=None:

    if dn:
      cl.append(
        form.applAnchor(
          'search','Up',sid,
          (
            ('dn',ParentDN(dn)),
            ('scope',w2lapp.searchform.SEARCH_SCOPE_STR_ONELEVEL),
            ('filterstr',u'(objectClass=*)'),
          ),
          title=u'Go up one level',
        )
      )

    cl.extend((
      form.applAnchor(
        'search','Down',sid,
        (
          ('dn',dn),
          ('scope',w2lapp.searchform.SEARCH_SCOPE_STR_ONELEVEL),
          ('filterstr',u'(objectClass=*)'),
        ),
        title=u'Descend into tree from here',
      ),
      form.applAnchor('searchform','Search',sid,
        (('dn',dn),),
        title=u'Enter search criteria in input form',
      ),
    ))

    if dn or ls.l.get_option(ldap.OPT_PROTOCOL_VERSION)>=ldap.VERSION3:
      cl.append(
        form.applAnchor(
          'read',
          {0:'Read',1:'[Root DSE]'}[dn==''],
          sid,[('dn',dn),('read_nocache','1')],
          title=u'Read the current entry',
        ),
      )
    cl.extend((
      form.applAnchor('addform','New entry',sid,[('dn',dn)],title=u'Add a new entry below here'),
      form.applAnchor('conninfo','ConnInfo',sid,[('dn',dn)],title=u'Show information about HTTP and LDAP connections'),
      form.applAnchor('ldapparams','Params',sid,[('dn',dn)],title=u'Tweak parameters used for LDAP operations (controls etc.)'),
      form.applAnchor('login','Bind',sid,[('dn',dn)],title=u'Login to directory'),
      form.applAnchor('oid','Schema',sid,[('dn',dn)],title=u'Browse/view subschema'),
    ))

    cl.append(form.applAnchor('disconnect','Disconnect',sid,[],title=u'Disconnect from LDAP server'))

  else:

    cl.append(form.applAnchor('','Connect',None,[],title=u'New connection to LDAP server'))

  return cl # MainMenu()


def DITNavigationList(sid,outf,form,ls,dn,level=0):
  dn_list=explode_dn(dn)[level:]
  dn_list_len = len(dn_list)
  result = [
    form.applAnchor(
      'read',
      form.utf2display(dn_list[i] or '[Root DSE]'),
      sid,
      [
        ('dn',','.join(dn_list[i:dn_list_len])),
#        ('scope',str(ldap.SCOPE_ONELEVEL)),
      ],
      title=u'Jump to %s' % (u','.join(dn_list[i:dn_list_len])),
    )
    for i in range(dn_list_len)
  ]
#  if dn:
  result.append(
    form.applAnchor(
      'read',
      '[Root DSE]',
      sid,
      [
        ('dn',''),
#        ('scope',str(ldap.SCOPE_ONELEVEL)),
      ],
      title=u'Jump to root DSE',
    )
  )
  return result # DITNavigationList()


def TopSection(sid,outf,form,ls,dn,title,main_menu_list,context_menu_list=[]):
  if not dn or not ldaputil.base.is_dn(dn):
    dn=u''
  if ls:
    uri = (ls.uri or '').decode('ascii')
  else:
    uri = u''
  web2ldap_title = ' - '.join(filter(None,[
    form.utf2display(uri),
    form.utf2display(dn),
    title
  ]))
  w2lapp.gui.PrintHeader(sid,outf,form,web2ldap_title,w2lapp.cnf.GetParam(ls,'link_css',''))
  outf.write('<div id="TopSection">\n')
  StatusLine(sid,outf,form,ls,dn)
  CommandTable(outf,main_menu_list,div_id='MainMenu')
  CommandTable(outf,context_menu_list,div_id='ContextMenu')
  outf.write('</div>\n')
  return # TopSection()


def SimpleMessage(
  sid,outf,form,ls,dn,
  title=u'',message=u'',
  p_id='Message',
  main_menu_list=[],context_menu_list=[]
):
  TopSection(sid,outf,form,ls,dn,title,main_menu_list,context_menu_list=context_menu_list)
  outf.write('<div id="%s" class="Main">\n%s\n</div>\n' % (p_id,message))
  w2lapp.gui.PrintFooter(outf,form)
  return # SimpleMessage()


# Return a pretty HTML-formatted string describing a schema element
# referenced by name or OID
def SchemaElementName(sid,form,dn,schema,se_nameoroid,se_class,name_template=r'%s'):
  result = [name_template % (se_nameoroid.encode())]
  if se_class:
    se = schema.get_obj(se_class,se_nameoroid,None)
    if not se is None:
      result.append(form.applAnchor(
          'oid','&raquo;',sid,
          [ ('dn',dn),('oid',se.oid),('oid_class',ldap.schema.SCHEMA_ATTR_MAPPING[se_class]) ]
      ))
  return '\n'.join(result)


def LDAPURLButton(sid,form,ls,data):
  if isinstance(data,LDAPUrl):
    l = data
  else:
    l = LDAPUrl(ldapUrl=data)
  command_func = {True:'read',False:'search'}[l.scope==ldap.SCOPE_BASE]
  if l.hostport:
    command_text = 'Connect'
    return form.applAnchor(
      command_func,
      'Connect and %s' % (command_func),
      None,
      (('ldapurl',unicode(str(l))),)
    )
  else:
    command_text = {True:'Read',False:'Search'}[l.scope==ldap.SCOPE_BASE]
    return form.applAnchor(
      command_func,command_text,sid,
      [
        ('dn',l.dn.decode(form.accept_charset)),
        ('filterstr',l.filterstr.decode(form.accept_charset)),
        ('scope',unicode(l.scope or ldap.SCOPE_SUBTREE)),
      ],
    )


def DataStr(sid,form,ls,dn,schema,attrtype_name,value,valueindex=0,commandbutton=0,entry=None):
  """
  Return a pretty HTML-formatted string of the attribute value
  """
  attr_instance = w2lapp.schema.syntaxes.syntax_registry.attrInstance(sid,form,ls,dn,schema,attrtype_name,value,entry)
  try:
    result = attr_instance.displayValue(valueindex,commandbutton)
  except UnicodeError:
    attr_instance = w2lapp.schema.syntaxes.OctetString(sid,form,ls,dn,schema,attrtype_name,value,entry)
    result = attr_instance.displayValue(valueindex,commandbutton)
  return result


# Ausdrucken eines HTML-Kopfes mit Titelzeile
def PrintHeader(sid,outf,form,TitleMsg,link_css):

  additional_http_header = {}
  additional_http_header.update(w2lapp.cnf.misc.http_headers)

  if form.env.get('HTTPS','off')=='on' and \
     not 'Strict-Transport-Security' in additional_http_header:
    additional_http_header['Strict-Transport-Security']='max-age=15768000 ; includeSubDomains'

  pyweblib.httphelper.SendHeader(
    outf,
    'text/html',
    form.accept_charset,
    expires_offset=w2lapp.cnf.misc.sec_expire,
    additional_header=additional_http_header,
  )
  html_begin_template_filename = GetVariantFilename(w2lapp.cnf.misc.html_begin_template,form.accept_language)
  html_begin_template_str = open(html_begin_template_filename,'rb').read()
  outf.write(html_begin_template_str.format(
    title_msg=TitleMsg.replace('<br>',''),
    refresh_time=str(w2lapp.cnf.misc.session_remove+10),
    base_url=form.script_name,
    web2ldap_version=w2lapp.__version__,
    accept_charset=form.accept_charset,
    link_css=link_css,
  ))
  return # PrintHeader()


def URLRedirect(
  outf,
  form,
  target_url,
  refresh_time=0,
  redirect_msg='Redirecting...',
  link_text='Click here to manually follow redirect'
):
  """
  Outputs HTML text with redirecting <head> section.
  """

  additional_http_header = {}
  additional_http_header.update(w2lapp.cnf.misc.http_headers)

  if form.env.get('HTTPS','off')=='on' and \
     not 'Strict-Transport-Security' in additional_http_header:
    additional_http_header['Strict-Transport-Security']='max-age=15768000 ; includeSubDomains'

  pyweblib.httphelper.SendHeader(outf,additional_header=additional_http_header)

  # Write out stub body with just a short redirect HTML snippet
  outf.write("""
<html>
  <head>
    <meta http-equiv="refresh" content="{refresh_time:d}; url={target_url}">
  </head>
  <body>
    <p>{redirect_msg}</p>
    <a href="{target_url}">{link_text}</a>
  </body>
</html>
    """.format(
      refresh_time = refresh_time,
      target_url = target_url,
      redirect_msg = redirect_msg,
      link_text = link_text,
    )
  )
  return # URLRedirect()

# Ausdrucken eines HTML-Endes
def PrintFooter(f,form):
  f.write('<a id="web2ldap_bottom"></a></body>\n</html>\n')
  f.flush()


def SearchRootField(form,ls,dn,name='dn',text=u'Search Root',default=None,search_root_searchurl=None):
  """Prepare input field for search root"""

  def sortkey_func(s):
    l = list(unicode(s).lower())
    l.reverse()
    return u''.join(l)

  if dn:
    dn_select_list = [dn]+ParentDNList(dn,ls.getSearchRoot(dn))
  else:
    dn_select_list = []
  dn_select_list = msbase.union(ls.namingContexts,dn_select_list,ignorecase=1)
  if search_root_searchurl:
    slu = ldapurl.LDAPUrl(search_root_searchurl.encode(ls.charset))
    try:
      ldap_result = ls.l.search_s(
        slu.dn,
        slu.scope,
        slu.filterstr,
        attrlist=['1.1'],
      )
    except ldap.LDAPError:
      pass
    else:
      dn_select_list = msbase.union(
        [
          ls.uc_decode(ldap_dn)[0]
          for ldap_dn,ldap_enty in ldap_result
        ],
        dn_select_list,
        ignorecase=1
      )
  dn_select_list.append((u'',u'- World -'))
  dn_select_list = list(set(dn_select_list))
  dn_select_list.sort(key=sortkey_func)
  srf = pyweblib.forms.Select(
    name,text,1,
    default=default or ls.getSearchRoot(dn),
    options=dn_select_list,
    ignoreCase=1
  )
  srf.setCharset(form.accept_charset)
  return srf # SearchRootField()


def ExceptionMsg(sid,outf,form,ls,dn,Heading,Msg):
  """
  Heading
    Unicode string with text for the <h1> heading
  Msg
    Raw string with HTML with text describing the exception
    (Security note: Must already be quoted/escaped!)
  """
  server_admin_bytes = form.utf2display(form.env.get('SERVER_ADMIN','').decode('ascii'))
  TopSection(sid,outf,form,ls,dn,'Error',MainMenu(sid,form,ls,dn),context_menu_list=[])
  outf.write("""
    <div class="Main">
      <h1>{heading}</h1>
      <div class="ErrorMessage">
        {error_msg}
      </div>
      <p>
        For questions or error reports contact:
        <a href="mailto:{server_admin}">{server_admin}</a>
      </p>
    </div>
    """.format(
      heading=form.utf2display(Heading),
      error_msg=Msg,
      server_admin=server_admin_bytes,
    )
  )
  PrintFooter(outf,form)
  return # ExceptionMsg()
