#!/usr/bin/env python
# -*- 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 People Server"""

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


import base64
import copy
import os
import sys
import time

glasnostPythonDir = '/usr/local/lib/glasnost-devel' # changed on make install
sys.path.insert(0, glasnostPythonDir)

import glasnost

import glasnost.common.context as context
from glasnost.common.PeopleCommon import *
import glasnost.common.faults as faults
import glasnost.common.system as system
import glasnost.common.tools_new as commonTools
import glasnost.common.xhtmlgenerator as X

from glasnost.server.ObjectsServer import register, ObjectServerMixin, \
        AdminServerMixin, ObjectsServer, ObjectsVirtualServer
from glasnost.server.tools import *

from glasnost.proxy.CacheProxy import invalidateValue
from glasnost.proxy.DispatcherProxy import getApplicationId
from glasnost.proxy.GroupsProxy import getSetContainedIds
from glasnost.proxy.tools import getProxy


applicationName = 'PeopleServer'
applicationRole = 'people'
dispatcher = None


class AdminPeople(AdminServerMixin, AdminPeopleCommon):
    pass
register(AdminPeople)


class Person(ObjectServerMixin, PersonCommon):
    pass
register(Person)


class PeopleVirtualServer(ObjectsVirtualServer):
    sendEmailToUsers = None

    def __getstate__(self):
        state = ObjectsVirtualServer.__getstate__(self)
        if state.has_key('sendEmailToUsers'):
            del state['sendEmailToUsers']
        return state

    def init(self):
        ObjectsVirtualServer.init(self)


class PeopleServer(PeopleCommonMixin, ObjectsServer):

    """Handle the people services.

    Attributes:
    ===========

    *VirtualServer*:
        The virtual server where the server is.
        
    *dataDirectoryPath*:
        The path where the poeple server non core datas are.

    *useAdminReaderSet*:
       Set if the people server uses readers set to allow reading.
        
    *useAdminWriterSet*:
        Set if the people server uses writers set to allow writing.

    *version*:
        The server version
    
    *objectClassName*:
        The class name of the people server.
    
    """
    
    VirtualServer = PeopleVirtualServer

    def addObjectXmlRpc(self, objectImport):
        """Create a new person on the server.
        
        Keyword argument:
        =================

        *objectImport*:
            The new person in XML RPC dictionnary format.
        
        Returns:
        ========

        The new person ID
        
        Exceptions:
        ===========

        *faults.UserAccessDenied*:
            The user is not in the admin set.

        *standard Exception*:
            - The thing category 'object' doesn't exists. (very grave !)
            - The adminImport __thingName__ key is not a valid Thing name.

        *KeyError*:
            The virtual server ID does not correspond to a instanciated
            virtual server.

        *faults.UnknownApplicationToken*:
            The given application token is not in the dispatcher virtual server
            ids dictionnary.

        *AttributeError*:
            + No groups server proxy found.

        *faults.MissingItem*:
            The Person object was not found.

        *faults.BadEmailAddress*:
            The Person email address is invalid.
            
        *faults.SmtpError*:
            The user was created, but the mail with his password cannot be
            send.
            (Problem with the SMTP gateway/server).

        """

        objectId = ObjectsServer.addObjectXmlRpc(self, objectImport)
        virtualServerId = context.getVar('applicationId')
        virtualServer = self.getVirtualServer(virtualServerId)
        object = virtualServer.loadObjectCore(objectId)
        if virtualServer.admin.defaultGroupId is not None:
            getProxyForServerRole('groups').addObjectMember(
                    virtualServer.admin.defaultGroupId, object.id)
        try:
            gpg = Gpg(email = object.email, fingerprint = object.fingerprint)
            gpg.addKey()
        except: # raise Exception('GnuPG not configured for the user')
            # it should be a real fault that could then be catched specifically
            pass
        return object.id

    def canDeleteObject(self, objectId):
        virtualServerId = context.getVar('applicationId')
        virtualServer = self.getVirtualServer(virtualServerId)
        if not virtualServer.canLoadObjectCore(objectId):
            return 0
        object = virtualServer.loadObjectCore(objectId)
        return self.isAdmin()
        # or objectId == getProxyForServerRole('identities').getPersonId()

    def canGetObject(self, objectId):
        clientToken = context.getVar('clientToken')
        clientId = getApplicationId(clientToken)
        clientRole = commonTools.extractRole(clientId)
        if clientRole == 'identities':
            return 1
        if ObjectsServer.canGetObject(self, objectId):
            return 1
        virtualServerId = context.getVar('applicationId')
        virtualServer = self.getVirtualServer(virtualServerId)
        if not virtualServer.canLoadObjectCore(objectId):
            return 0

        if objectId == getProxyForServerRole('identities').getPersonId():
            return 1
        
        return 0
        
    def canModifyObject(self, objectId):
        virtualServerId = context.getVar('applicationId')
        virtualServer = self.getVirtualServer(virtualServerId)
        if not virtualServer.canLoadObjectCore(objectId):
            return 0
        object = virtualServer.loadObjectCore(objectId)
        # FIXME: should ask identitiesServer if we are allowed to modify
        # this object
        return self.isAdmin() or \
            objectId == getProxyForServerRole('identities').getPersonId()

    def deleteObject(self, objectId):
        virtualServerId = context.getVar('applicationId')
        virtualServer = self.getVirtualServer(virtualServerId)
        object = virtualServer.loadObjectCore(objectId)
        if not self.canDeleteObject(objectId) \
                and not object.canBeDeletedByClient():
            raise faults.UserAccessDenied()
        try:
            gpg = Gpg(object.email)
            gpg.deleteKey()
        except: # tighter check
            pass
        object.removeNonCore()
        object.clear()
        del virtualServer.objects[objectId]
        virtualServer.markObjectAsDeleted(objectId)
        virtualServer.markCoreAsDirty()
        invalidateValue(objectId)

    def findObjectIdsXmlRpc(self, text):
        """Find person objects ID corresponding to a specified string.
        
        Keyword argument:
        =================
        
        *text*:
            The text that must be in the person object (conforming to the
            object containstext method).
            
        Return values:
        ==============
        
        *The corresponding objects IDs sequence*
            
        Exceptions:
        ===========

        *KeyError*:
            The virtual server ID does not correspond to a instanciated virtual
            server.
            
        """
        
        virtualServerId = context.getVar('applicationId')
        virtualServer = self.getVirtualServer(virtualServerId)
        text = iso8859_15(text)
        foundIds = []
        for objectId, object in virtualServer.objects.items():
            if object.containsText(text):
                foundIds.append(objectId)
        return foundIds


    def getObjectEmail(self, objectId):
        clientToken = context.getVar('clientToken')
        clientId = getApplicationId(clientToken)
        clientRole = commonTools.extractRole(clientId)
        if clientRole != 'identities':
            raise faults.ApplicationAccessDenied(clientId)
        
        virtualServerId = context.getVar('applicationId')
        virtualServer = self.getVirtualServer(virtualServerId)
        object = virtualServer.loadObjectCore(objectId)
        emailAddress = object.getEmail()
        if not emailAddress:
            return '' # or raise something ?

        return emailAddress

        
    def getObjectStringFromDigestXmlRpc(self, objectId, path, digest):
        """Retrieve a string in the specified object from its MD5 digest.
        
        Keyword arguments:
        ==================

        *path*:
            The path to the string location in the object. It could be the
            object itself, or a dictionnary or a sequence, etc.

        *digest*:
            The MD5 digest of the wanted string.

        Return value:
        =============
        
        *The the object string representation in UTF-8*
            
        Exceptions:
        ===========

        *faults.UserAccessDenied*:
            The user is not in the admin set.

        *TypeError*:
            The given path is None.
        
        *KeyError*:
            The virtual server ID does not correspond to a instanciated virtual
            server.
            
        *faults.MissingItem*:
            No person object corresponding to the login was not found.
        
        *faults.UnknownStringDigest*:
            No string corresponding to the given MD5 digest was found.
            
        """

        virtualServerId = context.getVar('applicationId')
        virtualServer = self.getVirtualServer(virtualServerId)
        object = virtualServer.loadObjectCore(objectId)
        object.acquireNonCore()
        try:
            # The objectLabel is accessible from everybody.
            if path == 'self.getLabel()':
                result = getStringFromDigest(object.getLabel(), digest)
            else:
                if not self.isAdmin() and not getProxyForServerRole(
                        'identities').setContainsUser([objectId]):
                    raise faults.UserAccessDenied()
                result = getStringFromDigest(
                        eval(path, {'self': object}), digest)
        finally:
            object.releaseNonCore()
        if result is None:
            raise faults.UnknownStringDigest(digest)
        return utf8(result)

    def modifyObjectXmlRpc(self, objectImport):
        """Modify an object.
        
        Keyword argument:
        =================

        *objectImport*:
            The modified person in XML RPC dictionnary format.
        
        Returns:
        ========

        The new version number of the modified object.
 
        Exceptions:
        ===========

        *faults.MissingItem*:
            The specified object does not exist.
            
        *faults.UserAccessDenied*:
            The client is not allowed to modify the object.

        *standard Exception*:
            - The thing category 'object' doesn't exists. (very grave !)
            - The adminImport __thingName__ key is not a valid Thing name.

        *KeyError*:
            The virtual server ID does not correspond to a instanciated virtual
            server.
        
        *faults.ReadOnlyObject*:
            The object is read only.

        """
        
        virtualServerId = context.getVar('applicationId')
        virtualServer = self.getVirtualServer(virtualServerId)
        objectChanges = commonTools.importThing(objectImport)
        object = virtualServer.loadObjectCore(objectChanges.id)

        object.acquireNonCore()
        try:
            if not object.canBeModified():
                raise faults.ReadOnlyObject()
            if not self.canModifyObject(object.id):
                if not object.canBeModifiedByClient():
                    raise faults.UserAccessDenied()
            object.checkModifyIsPossible(objectChanges)
            if object.fingerprint:
                try:
                    gpg = Gpg(objectChanges.email,
                              fingerprint = object.fingerprint)
                    gpg.updateKey()
                except: # tighter check
                    pass
            object.modify(objectChanges)
            object.saveNonCore()
        finally:
            object.releaseNonCore()
        virtualServer.markObjectAsDirty(object)
        invalidateValue(object.id)

        return object.version

    def registerPublicMethods(self):
        """Register the people server XML RPs."""

        ObjectsServer.registerPublicMethods(self)
        self.registerPublicMethod('findObjectIds', self.findObjectIdsXmlRpc)
        self.registerPublicMethod('getObjectEmail')

    def repairVirtualServer(self, virtualServer, version):
        """Handle a descendant compatibily with older server datas.
        
        This method can load datas stored by older server version.
        
        Keyword arguments:
        ==================

        *virtualServer*:
            The actual server instance.

        *version*:
            The datas version number.
            
        """

        changed = 0
        if version < 3004:
            import time
            for object in virtualServer.objects.values():
                if object.creationTime is None:
                    changed = 1
                    object.modificationTime = object.creationTime = time.time()
        if version < 4000:
            changed = virtualServer.admin.repair(4000) or changed
            for id, object in virtualServer.objects.items():
                newId = repairId(id)
                if newId:
                    changed = 1
                    del virtualServer.objects[id]
                    virtualServer.objects[newId] = object
                changed = object.repair(4000) or changed
        if version < 5004:
            changed = virtualServer.admin.repair(5004) or changed
            for id, object in virtualServer.objects.items():
                changed = object.repair(5004) or changed
        if version <= 1005000:
            changed = 1
            virtualServer.objectsByLogin = {}
            for id, object in virtualServer.objects.items():
                virtualServer.objectsByLogin[object.login] = object
        if version <= 1029000:
            admin = virtualServer.admin
            if admin.id is None:
                changed = 1
                admin.id = '%s/__admin__' % virtualServer.virtualServerId
        if version <= 1041000:
            for object in virtualServer.objects.values():
                if object.voteTokens is None:
                    continue
                for electionId, voteToken in object.voteTokens.items():
                    if not voteToken.startswith('glasnost://'):
                        changed = 1
                        object.voteTokens[electionId] = '%s/votes/%s' % (
                            commonTools.extractDispatcherId(
                            virtualServer.virtualServerId),
                            voteToken)
        if version <= 1123000:
            del virtualServer.objectsByLogin
            changed = 1
        if changed:
            virtualServer.markAllAsDirtyFIXME()

    def upgradeVirtualServer_0001_0019(self, virtualServer):
        ObjectsServer.upgradeVirtualServer_0001_0019(self, virtualServer)

        # Fill empty writersSet slots in admin. 
        admin = virtualServer.admin
        if admin.hasSlotName('writersSet') and not admin.writersSet:
            admin.writersSet = [system.generalPublicId]
            virtualServer.markAdminAsDirty(admin)


peopleServer = PeopleServer()


if __name__ == "__main__":
    peopleServer.launch(applicationName, applicationRole)

