# -*- coding: iso-8859-15 -*-


# Glasnost
# By: Odile Bénassy <obenassy@entrouvert.com>
#     Romain Chantereau <rchantereau@entrouvert.com>
#     Nicolas Clapiès <nclapies@easter-eggs.org>
#     Pierre-Antoine Dejace <padejace@entrouvert.be>
#     Thierry Dulieu <tdulieu@easter-eggs.com>
#     Florent Monnier <monnier@codelutin.com>
#     Cédric Musso <cmusso@easter-eggs.org>
#     Frédéric Péters <fpeters@entrouvert.be>
#     Benjamin Poussin <poussin@codelutin.com>
#     Emmanuel Raviart <eraviart@entrouvert.com>
#     Sébastien Régnier <regnier@codelutin.com>
#     Emmanuel Saracco <esaracco@easter-eggs.com>
#
# Copyright (C) 2000, 2001 Easter-eggs & Emmanuel Raviart
# Copyright (C) 2002 Odile Bénassy, Code Lutin, Thierry Dulieu, Easter-eggs,
#     Entr'ouvert, Frédéric Péters, Benjamin Poussin, Emmanuel Raviart,
#     Emmanuel Saracco & Théridion
# Copyright (C) 2003 Odile Bénassy, Romain Chantereau, Nicolas Clapiès,
#     Code Lutin, Pierre-Antoine Dejace, Thierry Dulieu, Easter-eggs,
#     Entr'ouvert, Florent Monnier, Cédric Musso, Ouvaton, Frédéric Péters,
#     Benjamin Poussin, Rodolphe Quiédeville, Emmanuel Raviart, Sébastien
#     Régnier, Emmanuel Saracco, Théridion & Vecam
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.


from __future__ import nested_scopes
    # lambdas are so much fun with nested scopes...


__doc__ = """Glasnost Web Tools"""

__version__ = '$Revision$'[11:-2]


import copy
import Cookie
import imp
import locale
import os
import re
from stat import *
import sys
import types
import urllib
import warnings

try:
    from mod_python import apache
except ImportError:
    apache = None

# zLOG is used by TAL that considers Zope is always there.  How wrong.
sys.modules['zLOG'] = imp.new_module('zLOG')

import glasnost.common.context as context
import glasnost.common.faults as faults
import glasnost.common.tools_new as commonTools
import glasnost.common.parsers as parsers
import glasnost.common.xhtmlgenerator as X

from glasnost.proxy.tools import *
import glasnost.web.tools_new as webTools

Fault = faults.Fault

_cachedWebsByServerRole = {}
currentTemplates = {}

HTTP_CONTINUE                     = 100
HTTP_SWITCHING_PROTOCOLS          = 101
HTTP_PROCESSING                   = 102
HTTP_OK                           = 200
HTTP_CREATED                      = 201
HTTP_ACCEPTED                     = 202
HTTP_NON_AUTHORITATIVE            = 203
HTTP_NO_CONTENT                   = 204
HTTP_RESET_CONTENT                = 205
HTTP_PARTIAL_CONTENT              = 206
HTTP_MULTI_STATUS                 = 207
HTTP_MULTIPLE_CHOICES             = 300
HTTP_MOVED_PERMANENTLY            = 301
HTTP_MOVED_TEMPORARILY            = 302
HTTP_SEE_OTHER                    = 303
HTTP_NOT_MODIFIED                 = 304
HTTP_USE_PROXY                    = 305
HTTP_TEMPORARY_REDIRECT           = 307
HTTP_BAD_REQUEST                  = 400
HTTP_UNAUTHORIZED                 = 401
HTTP_PAYMENT_REQUIRED             = 402
HTTP_FORBIDDEN                    = 403
HTTP_NOT_FOUND                    = 404
HTTP_METHOD_NOT_ALLOWED           = 405
HTTP_NOT_ACCEPTABLE               = 406
HTTP_PROXY_AUTHENTICATION_REQUIRED= 407
HTTP_REQUEST_TIME_OUT             = 408
HTTP_CONFLICT                     = 409
HTTP_GONE                         = 410
HTTP_LENGTH_REQUIRED              = 411
HTTP_PRECONDITION_FAILED          = 412
HTTP_REQUEST_ENTITY_TOO_LARGE     = 413
HTTP_REQUEST_URI_TOO_LARGE        = 414
HTTP_UNSUPPORTED_MEDIA_TYPE       = 415
HTTP_RANGE_NOT_SATISFIABLE        = 416
HTTP_EXPECTATION_FAILED           = 417
HTTP_UNPROCESSABLE_ENTITY         = 422
HTTP_LOCKED                       = 423
HTTP_FAILED_DEPENDENCY            = 424
HTTP_INTERNAL_SERVER_ERROR        = 500
HTTP_NOT_IMPLEMENTED              = 501
HTTP_BAD_GATEWAY                  = 502
HTTP_SERVICE_UNAVAILABLE          = 503
HTTP_GATEWAY_TIME_OUT             = 504
HTTP_VERSION_NOT_SUPPORTED        = 505
HTTP_VARIANT_ALSO_VARIES          = 506
HTTP_INSUFFICIENT_STORAGE         = 507
HTTP_NOT_EXTENDED                 = 510

OK = 0


def accessForbidden(dontAskForLogin = 0):
    userToken = context.getVar('userToken', default = '')
    if userToken or dontAskForLogin:
        layout = X.array()
        # TODO: nice text telling the user he should move away...
        return writePageLayout(layout, _('Access Forbidden!'), canCache = 0)

    cleanedUpUri = cleanUpUnparsedUri([], 'http')
    cleanedUpUri = cleanedUpUri.replace('/people/submit', '/').replace(
            '/index.py', '/')
    loginUrl = X.roleUrl('identities', action = 'login').add(
            'nextUri', cleanedUpUri).add('access', 'forbidden')
    hostNameAndPort = commonTools.makeHttpHostNameAndPort(
        context.getVar('httpHostName'),
        context.getVar('httpPort'))
    if context.getVar('virtualHost').useHTTPS:
        loginUrl = 'https://%(hostNameAndPort)s%(nameAndQuery)s' % {
                'hostNameAndPort': hostNameAndPort,
                'nameAndQuery': loginUrl,
                }
    return redirect(loginUrl)

def appendToUri(uri, item):
    if '?' in uri:
        separator = '&'
    else:
        separator = '?'
    return uri + separator + item


def cleanUpUnparsedUri(trashList, protocol = None):
    hostNameAndPort = commonTools.makeHttpHostNameAndPort(
        context.getVar('httpHostName'),
        context.getVar('httpPort'))
    pathAndQuery = commonTools.makeHttpPathAndQuery(
        context.getVar('httpPath'),
        context.getVar('httpQuery'))
    if pathAndQuery is None:
        pathAndQuery = ''
    if not hostNameAndPort:
        uri = pathAndQuery
    elif protocol == 'http':
        uri = 'http://%(hostNameAndPort)s%(pathAndQuery)s' % {
            'hostNameAndPort': hostNameAndPort,
            'pathAndQuery': pathAndQuery,
            }
    elif protocol == 'https':
        uri = 'https://%(hostNameAndPort)s%(pathAndQuery)s' % {
            'hostNameAndPort': hostNameAndPort,
            'pathAndQuery': pathAndQuery,
            }
    else:
        uri = pathAndQuery
    return cleanUpUri(uri, trashList)


def cleanUpUri(uri, trashList):
    for s in trashList:
        s = s + '='
        i = uri.find(s)
        if i >= 0:
            j = uri.find('&', i + len(s))
            if j < 0:
                s = uri[i:]
                if i > 0:
                    c = uri[i - 1]
                    if c in ['?', '&']:
                        uri = uri[:i - 1]
            else:
                uri = uri[:i] + uri[j + 1:]
    return uri


def confirmDelete(deleteUrl, id = None, objectName = None):
    backUrl = copy.copy(deleteUrl)
    backUrl.action = None # Remove the 'delete' action.

    if objectName is None:
        objectName = N_('entry')

    layout = X.form(action = deleteUrl, _class = 'delete-confirmation')
    
    if not id:
        message = _('Are you sure you want to delete the %s?') % _(objectName)
    else:
        message = _('Are you sure you want to delete the %s "%s" ?') % (
                    _(objectName),
                    getObjectLabelTranslated(id,
                            context.getVar('readLanguages')) )

    layout += X.p(message)
    layout += X.div(_class = 'buttons-bar')(
            X.buttonInForm('delete', deleteUrl),
            X.buttonStandalone('cancel', backUrl))

    return writePageLayout(layout, _('Confirm Deletion'), canCache = 0)


def failure(message, url = ''):
    layout = X.array()
    layout += X.p(_class = 'alert')(message)
    if url:
        layout += X.buttonStandalone('ok', url)
    return writePageLayout(layout, _('Failure'), canCache = 0)


def getDefaultDispatcherHostNameAndPort():
    return context.getVar('dispatcherId')[11:]


def getObject(objectId):
    web = getWeb(objectId)
    return web.getObject(objectId)

def getWeb(id):
    context.push(dispatcherId = commonTools.extractDispatcherId(id))
    try:
        web = getWebForServerRole(commonTools.extractRole(id))
    finally:
        context.pull()
    return web


def getWebForServerRole(serverRole):
    if context.getVar('knownRoles') and \
            not serverRole in context.getVar('knownRoles'):
        return None

    virtualHost = context.getVar('virtualHost')
    if virtualHost is not None and virtualHost.customWebs and \
            serverRole in virtualHost.customWebs.keys():
        serverRoleWeb = virtualHost.customWebs[serverRole].lower()
    else:
        serverRoleWeb = serverRole
    serverRoleWeb = serverRoleWeb.replace('-', '')

    if _cachedWebsByServerRole.has_key(serverRoleWeb):
        return _cachedWebsByServerRole[serverRoleWeb]

    # Code inspired from the module knee.
    import glasnost.web
    webFileNames = os.listdir(glasnost.web.__path__[0])
    for webFileName in webFileNames:
        if webFileName.endswith('Web.py') \
               and webFileName[:-6].lower() == serverRoleWeb:
            webName = webFileName[:-3]
            if hasattr(glasnost.web, webName):
                module = getattr(glasnost.web, webName)
            else:
                moduleTriplet = imp.find_module(
                        webName, glasnost.web.__path__)
                try:
                    module = imp.load_module(
                            'glasnost.web.%s' % webName,  moduleTriplet[0],
                            moduleTriplet[1],  moduleTriplet[2])
                finally:
                    if moduleTriplet[0]:
                        moduleTriplet[0].close()
                setattr(glasnost.web, webName, module)
            web = module.__dict__[webName]()
            break
    else:
        if 'ncards' in (context.getVar('knownRoles') or ['ncards']) and \
                serverRole not in ('ncards', 'sessions', 'translations'):
            ncardsProxy = getProxyForServerRole('ncards')
            try:
                ncard = ncardsProxy.getObjectByRole(serverRole)
            except faults.MissingItem:
                return getProxyForServerRole(serverRole)
            import NCardsWeb
            # TODO: cache this?
            return NCardsWeb.getWebClass(ncard)()
        web = getProxyForServerRole(serverRole)
    _cachedWebsByServerRole[serverRoleWeb] = web
    return _cachedWebsByServerRole[serverRoleWeb]


def isButtonSelected(buttonName, keywords):
    selected = 0
    if keywords.has_key('button') and keywords['button'] == buttonName \
       or keywords.has_key(buttonName) and keywords[buttonName] \
       or keywords.has_key(buttonName + '.x') and keywords[buttonName + '.x']:
        selected = 1
    if keywords.has_key('button') and keywords['button'] == buttonName:
        del keywords['button']
    if keywords.has_key(buttonName):
        del keywords[buttonName]
    if keywords.has_key(buttonName + '.x'):
        del keywords[buttonName + '.x']
    if keywords.has_key(buttonName + '.y'):
        del keywords[buttonName + '.y']
    return selected


def isTypeOfMimeType(mimeType, type):
    if mimeType is None or len(mimeType) < len(type):
        return 0
    return mimeType.startswith(type)


def pageNotFound():
    from mod_python import apache
    raise apache.SERVER_RETURN, HTTP_NOT_FOUND


def redirect(url):
    if type(url) == types.InstanceType and hasattr(url, 'getAsUrl'):
        url = url.getAsUrl()
    else:
        url = str(url)
    req = context.getVar('req')
    req.err_headers_out['Location'] = url
    setHttpCookie()
    req.status = HTTP_MOVED_TEMPORARILY
    req.send_http_header()
    return OK


def redirectPermanently(url):
    if type(url) == types.InstanceType and hasattr(url, 'getAsUrl'):
        url = url.getAsUrl()
    else:
        url = str(url)
    req = context.getVar('req')
    req.err_headers_out['location'] = url
    req.status = HTTP_MOVED_PERMANENTLY
    setHttpCookie()
    req.send_http_header()
    return OK


def rememberObject(id):
    identitiesWeb = getWebForServerRole('identities')
    identitiesWeb.rememberId(id)


def repairMimeType(fieldValue, fileName):
    # FIXME: Integrate this function in uploadFiles object
    # to correct mime type sent by the web browser.
    def getFileExt(fileName):
        return fileName[fileName.rfind('.')+1:]

    mimeTypesByExt = {
        'avi': 'video/x-msvideo',
        'bz2': 'application/bzip2',
        'doc': 'application/msword',
        'mpeg': 'video/mpeg ',
        'mpg': 'video/mpeg ',
        'mov': 'video/quicktime',
        'pdf': 'application/pdf',
        'ppt': 'application/vnd.ms-powerpoint',
        'ps': 'application/postscript',
        'qt': 'video/quicktime',
        'tar': 'application/x-tar',
        'tex': 'application/tex',
        'xls': 'application/vnd.ms-excel',
        'xml': 'text/xml ',
        'xsl': 'text/xml',
        'Z': 'application/zoo',
        }

    if len(fieldValue) < 12:
        return
    if fieldValue[12:] == 'octet-stream':
        ext = getFileExt()
        raise str(ext) + ' - ' + fieldValue + ' - ' + fileName
        if mimeTypesByExt.has_key(ext):
            fieldValue = mimeTypesByExt[getFileExt()]
            if fieldValue is not None:
                return fielValue
        return fieldValue


def replaceSpecialTags(text):
    text = text.replace('[{glasnost:defaultdispatchernameandport}]',
                        getDefaultDispatcherHostNameAndPort())

    localUrl = context.getVar('httpScriptDirectoryPath')
    if context.getVar('helpInUrl'):
        localUrl = '%shelp/' % localUrl
    text = text.replace('[{glasnost:localuri}]/', localUrl)
    text = text.replace('[{glasnost:localuri}]', localUrl)

    def aliasLabelReplacer(matchObject):
        alias = matchObject.group('alias')
        name = ''
        if 'pagenames' in context.getVar('knownRoles'):
            id = getProxyForServerRole('pagenames').getIdByName(alias)
            if id:
                try:
                    name = getObjectLabelTranslated(
                        id, context.getVar('readLanguages'))
                except faults.MissingItem:
                    name = _('(missing item)')
        name = name.replace('&', '&amp;')
        name = name.replace('<', '&lt;')
        name = name.replace('"', '&quot;')
        return name

    text = re.sub(
        r'\[{glasnost:aliaslabel:(?P<host>.*?):(?P<alias>.*?)}\]',
        aliasLabelReplacer, text)

    def aliasReplacer(matchObject):
        alias = matchObject.group('alias')
        action = matchObject.group('action')
        return X.aliasUrl(alias, action).getAsUrl()

    text = re.sub(
        r'\[{glasnost:alias:(?P<host>.*?):(?P<alias>.*?)(:(?P<action>.*?))?}\]',
        aliasReplacer, text)

    def imageReplacer(matchObject):
        host = matchObject.group('host')
        serverRole = matchObject.group('serverRole')
        localId = matchObject.group('localId')
        objectId = 'glasnost://%s/%s/%s' % (host, serverRole, localId)
        web = getWebForServerRole(serverRole)
        if web is None:
            return ''
        fileName = ''
        try:
            object = web.getPartialObject(objectId, ['dataFileName'])
            if hasattr(object, 'dataFileName') and object.dataFileName:
                fileName = '/%s' % object.dataFileName
        except faults.Fault:
            pass
        return X.idUrl(objectId, 'image%s' % fileName).getAsUrl()

    text = re.sub(
        '\[\{glasnost\:image\:(?P<host>.*?)\:(?P<serverRole>.*?)\:(?P<localId>.*?)\}\]',
        imageReplacer, text)

    def labelReplacer(matchObject):
        serverRole = matchObject.group('serverRole')
        localId = matchObject.group('localId')
        host = matchObject.group('host')
        try:
            name = getObjectLabelTranslated(
                'glasnost://%s/%s/%s' % (
                host, serverRole, localId),
                context.getVar('readLanguages'))
        except faults.MissingItem:
            name = _('(missing item)')
        name = name.replace('&', '&amp;')
        name = name.replace('<', '&lt;')
        name = name.replace('"', '&quot;')
        return name

    text = re.sub(
        '\[\{glasnost\:label\:(?P<host>.*?)\:(?P<serverRole>.*?)\:(?P<localId>.*?)\}\]',
        labelReplacer, text)

    def partialIdReplacer(matchObject):
        host = matchObject.group('host')
        serverRole = matchObject.group('serverRole')
        localId = matchObject.group('localId')
        action = matchObject.group('action')
        computedId =  'glasnost://%s/%s/%s' % (host, serverRole, localId)
        return X.idUrl(computedId, action).getAsUrl()

    text = re.sub(
            r'\[{glasnost:partialid:(?P<host>.*?):(?P<serverRole>.*?):'\
             '(?P<localId>.*?)(:(?P<action>.*))?}\]',
            partialIdReplacer, text)

    def thumbnailReplacer(matchObject):
        host = matchObject.group('host')
        serverRole = matchObject.group('serverRole')
        localId = matchObject.group('localId')
        width = matchObject.group('width')
        height = matchObject.group('height')
        web = getWebForServerRole(serverRole)
        if web is None:
            return ''
        return X.roleUrl(web.serverRole, 'thumbnail').add(
                'id',
                'glasnost://%s/%s/%s' % (host, serverRole, localId)).add(
                        'width', width).add('height', height).getAsUrl()
    text = re.sub(
            '\[\{glasnost\:thumbnail\:(?P<host>.*?)\:(?P<serverRole>.*?)\:(?P<localId>.*?)'
            '\:width=(?P<width>.*?)\:height=(?P<height>.*?)\}\]',
            thumbnailReplacer, text)

    canUseCookie = context.getVar('canUseCookie', default = 0)

    tag = urllib.quote('[{glasnost:sessiontoken}]')
    sessionToken = context.getVar('sessionToken')
    if not canUseCookie and sessionToken is not None:
        text = text.replace(tag, sessionToken)
    else:
        text = text.replace('?sessionToken=' + tag + '&', '?')
        text = text.replace('?sessionToken=' + tag, '')
        text = text.replace('&sessionToken=' + tag, '')

    return text


def setHttpCookie():
    if not context.getVar('canUseCookie', default = 0):
        return
    req = context.getVar('req')
    oldCookie = context.getVar('cookie')
    cookie = Cookie.SimpleCookie()
    cookieContent = {}
    sessionToken = context.getVar('sessionToken')
    if sessionToken is not None:
        cookieContent['sessionToken'] = sessionToken
    for k, v in cookieContent.items():
        cookie[k] = v
        cookie[k]['path'] = '/'
    if not cookieContent:
        if oldCookie:
            for k, v in oldCookie.items():
                cookie[k] = ''
                cookie[k]['path'] = '/'
                cookie[k]['max-age'] = 0
        else:
            cookie = None
    
    if cookie:
        sameCookie = 0
        if oldCookie and cookie.keys() == oldCookie.keys():
            for k, v in cookie.items():
                oldV = oldCookie[k]
                if v.value != oldV.value:
                    break
            else:
                sameCookie = 1
        
        if not sameCookie:
            for v in cookie.values():
                req.headers_out.add('Set-Cookie', v.output(header = '')[1:])
        context.setVar('cookie', cookie)


def success(message, url):
    layout = X.array()
    layout += X.p(_class = 'alert')(message)
    layout += X.buttonStandalone('ok', url)
    return writePageLayout(layout, _('Success'), canCache = 0)


import WebAPI

def getTemplateVars():
    userId = context.getVar('userId', default = '')
    if userId:
        userSet = [userId]
    else:
        userSet = None

    if context.getVar('virtualHost').useHTTPS:
        cleanedUpUri = cleanUpUnparsedUri([], 'https')
    else:
        cleanedUpUri = cleanUpUnparsedUri([])
    cleanedUpUri = cleanedUpUri.replace('/people/submit', '/').replace(
            '/index.py', '/')
    loginUrl = X.roleUrl('identities', action = 'login').add(
            'nextUri', cleanedUpUri)
    hostNameAndPort = commonTools.makeHttpHostNameAndPort(
        context.getVar('httpHostName'),
        context.getVar('httpPort'))
    if context.getVar('virtualHost').useHTTPS:
        loginUrl = 'https://%(hostNameAndPort)s%(nameAndQuery)s' % {
                'hostNameAndPort': hostNameAndPort,
                'nameAndQuery': loginUrl,
                }

    logoutUrl = X.roleUrl('identities', action = 'logout')

    aboutButton = X.buttonStandalone('about', X.roleUrl('about')).getAsXml()
    aboutUrl = X.roleUrl('about').getAsUrl()
    
    logoutButton = X.buttonStandalone('logout', logoutUrl).getAsXml()

    if userId:
        prefsButton = X.buttonStandalone('prefs', X.idUrl(userId)).getAsXml()
        prefsUrl = X.idUrl(userId).getAsUrl()
    
    loginButton = X.buttonStandalone('login', loginUrl).getAsXml()
    
    newAccountUrl = X.roleUrl('identities', action = 'newAccount')
    newAccountButton = X.buttonStandalone(
            'new-account', newAccountUrl).getAsXml()
    newAccountUrl = newAccountUrl.getAsUrl()

    shortNewsLabel = _('Short News')
    
    if userId:
        try:
            userName = getObject(userId).getLabel()
        except (faults.MissingItem, faults.UserAccessDenied):
            userName = _('Unknown')
    else:
        userName = ''

    return locals()

def writePageException(exception):
    req = context.getVar('req')
    exception = X.convertStringToXml(exception)
    req.write("""<html><head><title>Error</title></head>
    <body>
        <h1>Error processing this page</h1>
        <pre>
        %s
        </pre>
    </body>
    </html>""" % exception)


def writePageLayout(layout, title, canCache = 1):
    dict = getTemplateVars()
    dict['X'] = X
    dict['title'] = title
    
    req = context.getVar('req')
    req.content_type = 'text/html'

    setHttpCookie()
    req.send_http_header()

    if req.method == 'HEAD':
        return OK

    if req.caching:
        req.openCachePage()

    # we need to chdir for included files
    #os.chdir(context.getVar('webDirectoryPath'))
    os.chdir( context.getVar('templatesDirectoryPath') )
    
    dict['contextualHeaders'] = '\n'.join(context.getVar('htmlHeaders'))
    dict['body'] = layout.getAsXml()

    try:
        import GlasnostNewTal
    except ImportError:
        pass

    fileName = context.getVar('templateFileName')
    if not currentTemplates.has_key(fileName):
        # caching this in currentTemplates improves rendering time by about
        # 1.2 seconds on my (fpeters) computer.  Not bad
        ### (benchmark on 2002-11-16)
        templateDirectoryName = context.getVar('templateDirectoryName')
        path = context.getVar('templatesDirectoryPath')
        fallbackPath = context.getVar('fallbackTemplatesDirectoryPath')

        try:
            fd = open(os.path.join(path, templateDirectoryName, fileName))
        except IOError:
            fd = open(os.path.join(fallbackPath, 'default', fileName))
            # should raise 404 if it fails

        from TAL.HTMLTALParser import HTMLTALParser
        from glasnost.web.GlasnostTALGenerator import GlasnostTALGenerator
        t = HTMLTALParser(gen = GlasnostTALGenerator(xml=0))
        t.parseString(fd.read())
### with python-simpletal, it would be
###         t = GlasnostNewTal.compileHTMLTemplate(fd)

        currentTemplates[fileName] = t

    t = currentTemplates[fileName]

    program, macros = t.getCode()
    # this takes roughly 0.01 seconds, not worth caching
    
    from GlasnostTALInterpreter import GlasnostTALInterpreter
    from TAL.HTMLParser import HTMLParseError
    from GlasnostTALEngine import GlasnostTALEngine, TALError
    engine = GlasnostTALEngine(macros)
    context.setVar('talEngine', engine)
    engine.locals = WebAPI.getAPIDict()
    engine.locals['currentObject'] = context.getVar('currentObject')

    for k, v in dict.items():
        engine.locals[k] = v

    import traceback
    interp = GlasnostTALInterpreter(program, macros, engine, stream=req, wrap=80)
    try:
        interp()
    except TALError:
        if not context.getVar('debug'):
            raise
        info = sys.exc_info()
        exception = traceback.format_exception_only(
                info[0], info[1])[0].strip()
        writePageException(exception)

### with python-simpletal:
###     ctx = GlasnostNewTal.Context()
###     for k, v in dict.items() + WebAPI.getAPIDict().items():
###         ctx.addGlobal(k, v)
###     interpreter = GlasnostNewTal.TemplateInterpreter()
###     interpreter.initialise(ctx, req)
###     t.expand(ctx, req, interpreter = interpreter)

    if req.caching:
        req.closeCachePage()
    return OK


def getAppropriateTalFile(serverRole, idValue, action):
    if idValue:
        id = commonTools.splitId(idValue)
        serverRole = id[1]
        fileNames = (
            '%s-%s.%s.%s.tal' % (serverRole, id[0], id[2], action),
                # articles-our.website.url.211.print.tal
            '%s-%s.%s.tal' % (serverRole, id[0], action),
                # articles-our.website.url.print.tal
            '%s-%s.%s.tal' % (serverRole, id[2], action),
                # articles-211.print.tal
            '%s.%s.tal' % (serverRole, action),
                # articles.print.tal
            '%s-%s.%s.%s.xtal' % (serverRole, id[0], id[2], action),
                # articles-our.website.url.211.print.xtal
            '%s-%s.%s.xtal' % (serverRole, id[0], action),
                # articles-our.website.url.print.xtal
            '%s-%s.%s.xtal' % (serverRole, id[2], action),
                # articles-211.print.xtal
            '%s.%s.xtal' % (serverRole, action),
                # articles.print.xtal
            )
    else:
        id = ('',)
        fileNames = (
            '%s-%s.all.%s.tal' % (serverRole, id[0], action),
                # articles-our.website.url.211.print.tal
            '%s-all.%s.tal' % (serverRole, action),
                # articles-all.print.tal
            '%s-%s.all.%s.xtal' % (serverRole, id[0], action),
                # articles-our.website.url.211.print.xtal
            '%s-all.%s.xtal' % (serverRole, action),
                # articles-all.print.xtal
            )

    directories = context.getVar('webDirectoryPaths') + \
            [context.getVar('templatesDirectoryPath')]
    for directory in directories:
        if not directory:
            continue
        for fileName in fileNames:
            fileName = directory + '/' + fileName
            if os.path.exists(fileName):
                return fileName

    return None


def getAppropriatePyFile(serverRole, idValue, action):
    if idValue:
        id = commonTools.splitId(idValue)
        serverRole = id[1]
        fileNames = (
            '%s-%s.%s.%s.py' % (serverRole, id[0], id[2], action),
                # articles-our.website.url.211.print.py
            '%s-%s.%s.py' % (serverRole, id[0], action),
                # articles-our.website.url.print.py
            '%s-%s.%s.py' % (serverRole, id[2], action),
                # articles-211.print.py
            '%s.%s.py' % (serverRole, action),
                # articles.print.py
            )
    else:
        id = ('',)
        fileNames = (
            '%s-%s.all.%s.py' % (serverRole, id[0], action),
                # articles-our.website.url.211.print.py
            '%s-all.%s.py' % (serverRole, action),
                # articles-all.print.py
            )

    directoryPaths = context.getVar('scriptsDirectoryPaths')
    for directoryPath in directoryPaths:
        for fileName in fileNames:
            filePath = os.path.join(directoryPath, fileName)
            if os.path.exists(filePath):
                return filePath
    return None


def getAppropriateFunction(serverRole, idValue, action):
    if not action:
        action = 'default'

    web = context.getVar('web')

    talFileName = getAppropriateTalFile(serverRole, idValue, action)

    if talFileName:
        context.setVar('TALFile', open(talFileName).read())
        context.setVar(
            'TALFileIsXTAL', talFileName.endswith('.xtal'))
        if idValue:
            return web.viewTal
        else:
            return web.viewAllTal

    pyFileName = getAppropriatePyFile(serverRole, idValue, action)

    if pyFileName:
        context.setVar('PyFile', pyFileName)
        if idValue:
            return web.viewPy
        else:
            return web.viewAllPy
    
    method = context.getVar('httpMethod')
    if action == 'default':
        if idValue:
            if method == 'DELETE':
                action = 'delete'
            else:
                action = 'use'
        else:
            action = 'viewAll'

    if context.getVar('xmlPost', default = None) and action == 'use':
        action = 'postXml'
    
    if hasattr(web, action):
        function = getattr(web, action)
        if not hasattr(function, 'isPublicForWeb'):
            return None
        if method in ('GET', 'HEAD', 'POST') and function.isPublicForWeb == 1:
            return function
        if type(function.isPublicForWeb) in (list, tuple) and \
                method in function.isPublicForWeb:
            return function
        elif method not in ('GET', 'HEAD', 'POST'):
            raise apache.SERVER_RETURN, apache.HTTP_METHOD_NOT_ALLOWED
    return None


def getGotoObjectsLabelsAndLinks():
    labelsAndLinks = []

    # Standard servers.
    roles = context.getVar('knownRoles')
    for role in roles:
        try:
            web = getWebForServerRole(role)
        except: # TODO: tighter check.
            continue
        if not web:
            continue
        if not web.canViewAll():
            continue
        labelsAndLinks.append(
                (web.getTranslatedObjectsNameCapitalized(), X.roleUrl(role)) )

    # Cards Models.
    cardsWeb = getWebForServerRole('cards')
    if cardsWeb is not None:
        cardIds = cardsWeb.getAdmin().prototypeIds
        if cardIds:
            cardLabels = cardsWeb.getObjectLabelsTranslated(
                    cardIds, context.getVar('readLanguages'))
            for cardId, cardLabel in cardLabels.items():
                labelsAndLinks.append((cardLabel,
                                       X.idUrl(cardId, 'implementations')))

    labelsAndLinks.sort(lambda x, y: locale.strcoll(x[0], y[0]))
    return labelsAndLinks


def getNewObjectLabelsAndLinks():
    labelsAndLinks = []

    # Standard servers.
    roles = context.getVar('knownRoles')
    for role in roles:
        try:
            web = getWebForServerRole(role)
        except: # TODO: tighter check.
            continue
        if not web:
            continue
        try:
            if not web.canAddObject():
                continue
        except: # TODO: tighter check.
            continue
        labelsAndLinks.append(
                (web.getTranslatedObjectNameCapitalized(),
                X.roleUrl(role, 'edit')) )

    # Cards Models.
    cardsWeb = getWebForServerRole('cards')
    if cardsWeb is not None:
        cardIds = cardsWeb.getAdmin().prototypeIds
        if cardIds:
            cardLabels = cardsWeb.getObjectLabelsTranslated(
                    cardIds, context.getVar('readLanguages'))
            for cardId, cardLabel in cardLabels.items():
                labelsAndLinks.append((cardLabel, X.idUrl(cardId, 'new')))

    labelsAndLinks.sort(lambda x, y: locale.strcoll(x[0], y[0]))
    return labelsAndLinks

 
def processTALFile(fileName, file = None, xtal = None, **keywords):
    req = context.getVar('req')
    if xtal:
        req.content_type = 'text/xml'
    else:
        req.content_type = 'text/html'
    setHttpCookie()
    req.send_http_header()
    if req.method == 'HEAD':
        return OK

    if req.caching:
        req.openCachePage()

    for dir in context.getVar('webDirectoryPaths'):
        os.chdir(dir)
        if os.path.exists(fileName):
            file = open(fileName).read()
            break
    
    from TAL.TALParser import TALParser
    from TAL.HTMLTALParser import HTMLTALParser
    from glasnost.web.GlasnostTALGenerator import GlasnostTALGenerator
    if xtal:
        t = TALParser(gen = GlasnostTALGenerator(xml=1))
    else:
        t = HTMLTALParser(gen = GlasnostTALGenerator(xml=0))
    t.parseString(file)
    program, macros = t.getCode()

    from GlasnostTALInterpreter import GlasnostTALInterpreter
    from GlasnostTALEngine import GlasnostTALEngine, TALError
    engine = GlasnostTALEngine(macros)
    context.setVar('talEngine', engine)
    engine.locals = WebAPI.getAPIDict()
    engine.locals['currentObject'] = None
    for k, v in keywords.items():
        engine.locals[k] = v
    dict = getTemplateVars()
    for k, v in dict.items():
        engine.locals[k] = v

    import traceback
    interp = GlasnostTALInterpreter(program, macros, engine, stream=req, wrap=80)
    try:
        interp()
    except TALError:
        if not context.getVar('debug'):
            raise
        info = sys.exc_info()
        exception = traceback.format_exception_only(
                info[0], info[1])[0].strip()
        writePageException(exception)

    if req.caching:
        req.closeCachePage()
    return OK


def processXTALFile(fileName, file = None, **keywords):
    return processTALFile(fileName, file = file, xtal = 1, **keywords)


def spellcheck(text, language = 'en'):
    if not context.getVar('enableSpellChecking'):
        return text

    # generate a temp file name
    warnings.filterwarnings('ignore','tmpnam')
    import posix
    tmpnam = posix.tmpnam()

    # write text out to a temporary file
    tmp = open(tmpnam,'w')
    tmp.write(text)
    tmp.close()

    import commands
    # call out to aspell program
    status, aspell = commands.getstatusoutput(
            'aspell --lang=%s --home-dir=/tmp/ -H -a < %s' % (
                    language, tmpnam))
    os.remove(tmpnam)
    if status != 0:
        return text

    # split out all questionable words and non-html text
    aspell = re.compile('^& (\w+) \d+ \d+: (.*)', re.M).findall(aspell)
    text = re.compile('(<.*?>|&.*?;)', re.S).split(text)
    spanSpellcheck = re.compile('<span class="spellcheck".*?</span>')

    i = 0
    while i < len(text):
        if text[i].endswith('/>') and i%2-1:
            text.insert(i+1, '')
            text.insert(i, '')
            i += 2
        i += 1

    # substitute questionable words in text with a titled span
    alreadySeen = []
    for found, suggest in aspell:
        if found in alreadySeen:
            continue
        alreadySeen.append(found)
        match = re.compile(r'\b%s\b' % found)
        replace = '<span class="spellcheck" title="%s">%s</span>' % (
                suggest, found)
        for i in range(0, len(text), 2):
            t, n = match.subn(replace, text[i])
            if not n:
                continue
            parts = spanSpellcheck.split(t)
            parts.reverse()
            del text[i]
            for p in parts[:-1]:
                text.insert(i, p)
                text.insert(i, replace)
            text.insert(i, parts[-1])

    # return the concatenated result
    return ''.join(text)


def xmlEncode(string):
    for c in string:
        if ord(c) > 128:
            string = string.replace(c, '&#%s;' % ord(c))
    return string

