# -*- 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.


__doc__ = """Glasnost Common Tools"""

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


import imp
import md5
import re
import os
import smtplib
import types
try:
    from xml.dom import minidom
except ImportError:
    minidom = None
import new

try:
    import glasnost
except ImportError:
    class G:
        applicationName = 'glasnost'
    glasnost = G()

import faults
import context
import tools_new as commonTools

is8bit = re.compile("[\x80-\xff]").search


def extractApplicationHostName(id):
    """Return the application host name part of a token or application ID.

    Keyword argument:
    =================

    *id*:
        The application id /token string.

    *Return the application host name string*.

    Exception:
    ==========

    *Exception*:
        The *id* is malformed.
        The string has no fields separated by ':'.

    *Warning*:
    ==========

    Some tokens are malformed, so be careful.

    """
    
    try:
        return extractApplicationHostNameAndPort(id).split(':')[0]
    except IndexError:
        raise Exception('Malformed id or application id = %s' % str(id))


def extractApplicationHostNameAndPort(id):
    try:
        assert id.startswith('glasnost://')
        splittedApplicationId = id[11:].split('/', 1)
        # applicationHostNameAndPort, applicationRole = splittedApplicationId
        return splittedApplicationId[0]
    except IndexError:
        raise Exception('Malformed id or application id = %s' % str(id))


def extractApplicationPort(id):
    infos = id[11:].split(':', 1)
    if len(infos) >= 2:
        try:
            return int(infos[1])
        except (TypeError, ValueError):
            pass
    return context.getVar('dispatcherPort')


_cachedCommonsByServerRole = {}

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

    if _cachedCommonsByServerRole.has_key(serverRole):
        return _cachedCommonsByServerRole[serverRole]

    # Code inspired from the module knee.
    import glasnost.common
    commonFileNames = os.listdir(glasnost.common.__path__[0])
    for commonFileName in commonFileNames:
        if commonFileName.endswith('Common.py') \
               and commonFileName[:-9].lower() == serverRole:
            commonName = commonFileName[:-3]
            if hasattr(glasnost.common, commonName):
                module = getattr(glasnost.common, commonName)
            else:
                moduleTriplet = imp.find_module(
                        commonName, glasnost.common.__path__)
                try:
                    module = imp.load_module(
                            'glasnost.common.%s' % commonName,
                            moduleTriplet[0], moduleTriplet[1],
                            moduleTriplet[2])
                finally:
                    if moduleTriplet[0]:
                        moduleTriplet[0].close()
                setattr(glasnost.common, commonName, module)
            common = module.__dict__[commonName + 'Mixin']()
            break
    else:
        if 'ncards' in (context.getVar('knownRoles') or ['ncards']) and \
                serverRole not in ('ncards', 'sessions', 'translations'):
            import glasnost.proxy.NCardsProxy as NCardsProxy
            ncardsProxy = NCardsProxy.NCardsProxy()
            try:
                ncard = ncardsProxy.getObjectByRole(serverRole)
            except faults.MissingItem:
                return None
            import NCardsCommon
            return NCardsCommon.getCommonMixinClass(ncard)()
        return None
    _cachedCommonsByServerRole[serverRole] = common
    return _cachedCommonsByServerRole[serverRole]


def getStringFromDigest(object, digest):
    """Extract a string from an object via the MD5 digest.

    Keyword arguments:
    ==================

    *object*:
        + Formelly the path to the string location
        
        + A object containing the string.

        + A dictionnary of path (key or value).

        + A Sequence of path.

        + A string (carriage return are ommitted).

    Return:
    =======

    *None*:
        No corresponding string found.

    *String*:
        The string that produce the given MD5 digest (without the carriage
        returns).
    
    """
    
    objectType = type(object)
    if objectType in [types.StringType, types.UnicodeType]:
        if md5.new(object.replace('\r\n', '\n')).hexdigest() == digest:
            return object
        else:
            return None
    elif objectType in [types.ListType, types.TupleType]:
        for item in object:
            result = getStringFromDigest(item, digest)
            if result is not None:
                return result
    elif objectType == types.DictionaryType:
        for key, value in object.items():
            result = getStringFromDigest(key, digest)
            if result is not None:
                return result
            result = getStringFromDigest(value, digest)
            if result is not None:
                return result
        return None
    elif objectType == types.InstanceType:
        for key, value in object.__dict__.items():
            result = getStringFromDigest(key, digest)
            if result is not None:
                return result
            result = getStringFromDigest(value, digest)
            if result is not None:
                return result
        return None
    else:
        return None


def iso8859_15(s):
    """Convert a string from utf-8 to iso-8859-15."""
    # FIXME: due to yet untraced errors I switched from latin-0 to latin-1
    if type(s) == types.UnicodeType:
        return s.encode('latin-1')
    elif is8bit(s):
        try:
            return unicode(s, 'utf-8').encode('latin-1')
        except UnicodeError:
            return s
    else:
        return s


def repairId(id):
    if not id or id.startswith('glasnost://'):
        return  None
    try:
        serverId, localId = id.split('/', 1)
        assert serverId.startswith('ee')
        assert serverId.endswith('Server')
        serverRole = serverId[2:-6].lower()
    except IndexError:
        raise Exception('Malformed old id = %s' % str(id))
    newId = '%s/%s/%s' % (context.getVar('dispatcherId'), serverRole, localId)
    return newId


def sendMail(mailFrom, mailTo, mailSubject, mailMessage, mailPerson = None,
             moreHeaders = None):
    """Send a Email.

    Keyword arguments:
    ==================

    *mailFrom*:
        The sender email address field string.

    *mailTo*:
        The destination address(es) field string.

    *mailSubject*:
        The mail subject line string.
        
    *mailMessage*:
        The message string to send.

    *mailPerson*:
        The recipient's person or identity.
        
    *moreHearders*:
        If other hearders have to be included in the mail, this dictionnary
        provide them. The key is the field name, the value, the field value.
        Default: None.
        
    Exception:
    ==========

    *faults.SmtpError*:
        The mail cannot be send. (Problem with the SMTP gateway/server).
    
    """
    
    if type(mailTo) in [types.StringType, types.UnicodeType]:
        mailTo = [mailTo]

    mailTo = [x for x in mailTo if not x.endswith('@example.com')]
    if len(mailTo) == 0:
        return

    mailToStr = ', '.join(mailTo)

    mailHeader = "From: %s\nTo: %s\nSubject: %s\nMime-Type: 1.0\n" % (
        mailFrom, mailToStr, mailSubject)
    if moreHeaders:
        mailHeader += '\n'.join([ '%s: %s' % x for x in moreHeaders.items()])

    mailMessage = "\n" + mailMessage
    gpgEncrypted = 0
    if mailPerson:
        cryptEmails = mailPerson.mustCryptEmails()
        if cryptEmails:
            try:
                from gpg import Gpg
                gpg = Gpg(email = mailPerson.getEmail(),
                          fingerprint = mailPerson.getFingerprint())
                gpg.deleteKey()
                gpg.addKey()
                mailMessage = gpg.encrypt(mailMessage)
                gpgEncrypted = gpg.isEncrypted(mailMessage)
            except: # TODO: tighter check
                pass
            if gpgEncrypted:
                mailHeader += """\
Content-Type: multipart/encrypted; boundary=bound1;
  protocol=\"application/pgp-encrypted\"; 

--bound1
Content-Type: application/pgp-encrypted

Version: 1

--bound1
Content-Type: application/octet-stream

"""
    mailMessage = "%s\n%s" % (mailHeader, mailMessage)
    if gpgEncrypted:
        mailMessage += "\n--bound1--\n"
    mailMessage = mailMessage.replace('\n', '\r\n')

    smtpServerHostName = context.getVar('smtpServerHostName')
    smtpServerPort = context.getVar('smtpServerPort')

    try:
        smtpServer = smtplib.SMTP()
        smtpServer.connect(smtpServerHostName, smtpServerPort)
        smtpServer.sendmail(mailFrom, mailTo, mailMessage)
        smtpServer.quit()
    except: # TODO: tighter check
        raise faults.SmtpError()


def utf8(s):
    """Convert a string from iso-8859-15 to utf-8."""
    if type(s) == types.UnicodeType:
        return s.encode('utf-8')
    elif is8bit(s):
        return unicode(s, 'latin-1').encode('utf-8')
    else:
        return s


# Useful code cribbed from Mark Pilgrim:
#  http://diveintomark.org/archives/2003/01/27/dynamically_extending_apis.html

# add useful 'text' method to minidom.Element class
def _text(self):
    """returns all text of a node in one string

    text may be split into several Text nodes; minidom likes to create
    separate Text nodes for carriage returns and ampersands and things"""
    def isTextNode(node):
        return isinstance(node, minidom.Text)
    def getData(node):
        return node.data
    try:
        return "".join(map(getData, filter(isTextNode, self.childNodes)))
    except:
        return ""

if minidom:
    minidom.Element.text = new.instancemethod(_text, None, minidom.Element)

class XmlToDict:
    """Map XML elements into an easier to use dictionary.

    Add known namespaces URIs to be processed via knownNamespaces
    I.e.
       XmlToDict.knownNamespaces["URI"] = prefix"""
    separator = ':'

    def __init__(self):
        self.knownNamespaces = { u'http://purl.org/dc/elements/1.1/' : u'dc' }

    def isKnownElementNode(self, node):
        return isinstance(node, minidom.Element) and \
                ((node.namespaceURI == None and -1 == node.tagName.find(":") ) or \
                self.knownNamespaces.has_key(node.namespaceURI))

    def knownNameValuePair(self, element):
        tagName = element.tagName.strip()
        tagNameParts = tagName.split(":")
        if len(tagNameParts) == 2 and \
                self.knownNamespaces.has_key(element.namespaceURI) and \
                self.knownNamespaces != None:
            tagName = self.knownNamespaces[element.namespaceURI] + \
                        self.separator + tagNameParts[1]
        return (tagName, element.text())

    def mapElementsToDictionary(self, nodeList):
        return dict(map(self.knownNameValuePair, \
                            filter(self.isKnownElementNode, nodeList)))

def convertNodesToDict(nodeList, knownNamespaces = {}):
    """This function takes a list of nodes and returns a
       dictionary that maps nodes names to node values. The optional
       'knownNamespaces' provides a way to map known namespace URIs
       to fixed prefixes."""
    converter = XmlToDict()
    for (k, v) in knownNamespaces.iteritems():
        converter.knownNamespaces[k] = v
    return converter.mapElementsToDictionary(nodeList)

