view src/server/server.py @ 961:22fe06569b1a

server: moved logging worflow in separated method, so it can be used by Libervia Pages
author Goffi <goffi@goffi.org>
date Fri, 27 Oct 2017 18:35:23 +0200
parents 968eda9e982a
children c7fba7709d05
line wrap: on
line source

#!/usr/bin/python
# -*- coding: utf-8 -*-

# Libervia: a Salut à Toi frontend
# Copyright (C) 2011-2016 Jérôme Poisson <goffi@goffi.org>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from twisted.application import service
from twisted.internet import reactor, defer
from twisted.web import server
from twisted.web import static
from twisted.web import resource as web_resource
from twisted.web import util as web_util
from twisted.web import http
from twisted.python.components import registerAdapter
from twisted.python import failure
from twisted.words.protocols.jabber import jid

from txjsonrpc.web import jsonrpc
from txjsonrpc import jsonrpclib

from sat.core.log import getLogger
log = getLogger(__name__)
from sat_frontends.bridge.dbus_bridge import Bridge, BridgeExceptionNoService, const_TIMEOUT as BRIDGE_TIMEOUT
from sat.core.i18n import _, D_
from sat.core import exceptions
from sat.tools import utils
from sat.tools.common import regex
from sat.tools.common import template
from sat.tools.common import uri as common_uri

import re
import glob
import os.path
import sys
import tempfile
import shutil
import uuid
import urlparse
import urllib
from httplib import HTTPS_PORT
import libervia

try:
    import OpenSSL
    from twisted.internet import ssl
except ImportError:
    ssl = None

from libervia.server.constants import Const as C
from libervia.server.blog import MicroBlog
from libervia.server import session_iface


# following value are set from twisted.plugins.libervia_server initialise (see the comment there)
DATA_DIR_DEFAULT = OPT_PARAMETERS_BOTH = OPT_PARAMETERS_CFG = coerceDataDir = None




class LiberviaSession(server.Session):
    sessionTimeout = C.SESSION_TIMEOUT

    def __init__(self, *args, **kwargs):
        self.__lock = False
        server.Session.__init__(self, *args, **kwargs)

    def lock(self):
        """Prevent session from expiring"""
        self.__lock = True
        self._expireCall.reset(sys.maxint)

    def unlock(self):
        """Allow session to expire again, and touch it"""
        self.__lock = False
        self.touch()

    def touch(self):
        if not self.__lock:
            server.Session.touch(self)


class ProtectedFile(static.File):
    """A static.File class which doens't show directory listing"""

    def directoryListing(self):
        return web_resource.NoResource()


class LiberviaRootResource(ProtectedFile):
    """Specialized resource for Libervia root

    handle redirections declared in sat.conf
    """

    def __init__(self, options, *args, **kwargs):
        """
        @param options(dict): configuration options, same as Libervia.options
        """
        super(LiberviaRootResource, self).__init__(*args, **kwargs)

        ## redirections
        self.redirections = {}
        if options['url_redirections_dict'] and not options['url_redirections_profile']:
            raise ValueError(u"url_redirections_profile need to be filled if you want to use url_redirections_dict")

        for old, new_data in options['url_redirections_dict'].iteritems():
            # new_data can be a dictionary or a unicode url
            if isinstance(new_data, dict):
                # new_data dict must contain either "url" or "path" key (exclusive)
                # if "path" is used, a file url is constructed with it
                try:
                    new = new_data['url']
                except KeyError:
                    try:
                        path = new_data['path']
                    except KeyError:
                        raise ValueError(u'if you use a dict for url_redirections data, it must contain the "url" or a "file" key')
                    else:
                        new = 'file:{}'.format(urllib.quote(path))
                else:
                    if 'path' in new_data:
                        raise ValueError(u'You can\'t have "url" and "path" keys at the same time in url_redirections')
            else:
                new = new_data
                new_data = {}

            # some normalization
            if not old.strip():
                # root URL special case
                old = ''
            elif not old.startswith('/'):
                raise ValueError(u"redirected url must start with '/', got {}".format(old))
            else:
                old = self._normalizeURL(old)
            new_url = urlparse.urlsplit(new.encode('utf-8'))

            # we handle the known URL schemes
            if new_url.scheme == 'xmpp':
                # XMPP URI
                parsed_qs = urlparse.parse_qs(new_url.geturl())
                try:
                    item = parsed_qs['item'][0]
                    if not item:
                        raise KeyError
                except (IndexError, KeyError):
                    raise NotImplementedError(u"only item for PubSub URI is handled for the moment for url_redirections_dict")
                location = "/blog/{profile}/{item}".format(
                    profile=urllib.quote(options['url_redirections_profile'].encode('utf-8')),
                    item = urllib.quote_plus(item),
                    ).decode('utf-8')
                request_data = self._getRequestData(location)

            elif new_url.scheme in ('', 'http', 'https'):
                # direct redirection
                if new_url.netloc:
                    raise NotImplementedError(u"netloc ({netloc}) is not implemented yet for url_redirections_dict, it is not possible to redirect to an external website".format(
                        netloc = new_url.netloc))
                location = urlparse.urlunsplit(('', '', new_url.path, new_url.query, new_url.fragment)).decode('utf-8')
                request_data = self._getRequestData(location)

            elif new_url.scheme in ('file'):
                # file or directory
                if new_url.netloc:
                    raise NotImplementedError(u"netloc ({netloc}) is not implemented for url redirection to file system, it is not possible to redirect to an external host".format(
                        netloc = new_url.netloc))
                path = urllib.unquote(new_url.path)
                if not os.path.isabs(path):
                    raise ValueError(u'file redirection must have an absolute path: e.g. file:/path/to/my/file')
                # for file redirection, we directly put child here
                segments, dummy, last_segment = old.rpartition('/')
                url_segments = segments.split('/') if segments else []
                current = self
                for segment in url_segments:
                    resource = web_resource.NoResource()
                    current.putChild(segment, resource)
                    current = resource
                resource_class = ProtectedFile if new_data.get('protected',True) else static.File
                current.putChild(last_segment, resource_class(path))
                log.debug(u"Added redirection from /{old} to file system path {path}".format(old=old.decode('utf-8'), path=path.decode('utf-8')))
                continue # we don't want to use redirection system, so we continue here

            else:
                raise NotImplementedError(u"{scheme}: scheme is not managed for url_redirections_dict".format(scheme=new_url.scheme))

            self.redirections[old] = request_data
            if not old:
                log.info(u"Root URL redirected to {uri}".format(uri=request_data[1].decode('utf-8')))

        # no need to keep url_redirections*, they will not be used anymore
        del options['url_redirections_dict']
        del options['url_redirections_profile']

        # the default root URL, if not redirected
        if not '' in self.redirections:
            self.redirections[''] = self._getRequestData(C.LIBERVIA_MAIN_PAGE)

    def _normalizeURL(self, url, lower=True):
        """Return URL normalized for self.redirections dict

        @param url(unicode): URL to normalize
        @param lower(bool): lower case of url if True
        @return (str): normalized URL
        """
        if lower:
            url = url.lower()
        return '/'.join((p for p in url.encode('utf-8').split('/') if p))

    def _getRequestData(self, uri):
        """Return data needed to redirect request

        @param url(unicode): destination url
        @return (tuple(list[str], str, str, dict): tuple with
            splitted path as in Request.postpath
            uri as in Request.uri
            path as in Request.path
            args as in Request.args
        """
        uri = uri.encode('utf-8')
        # XXX: we reuse code from twisted.web.http.py here
        #      as we need to have the same behaviour
        x = uri.split(b'?', 1)

        if len(x) == 1:
            path = uri
            args = {}
        else:
            path, argstring = x
            args = http.parse_qs(argstring, 1)

        # XXX: splitted path case must not be changed, as it may be significant
        #      (e.g. for blog items)
        return self._normalizeURL(path, lower=False).split('/'), uri, path, args

    def _redirect(self, request, request_data):
        """Redirect an URL by rewritting request

        this is *NOT* a HTTP redirection, but equivalent to URL rewritting
        @param request(web.http.request): original request
        @param request_data(tuple): data returned by self._getRequestData
        @return (web_resource.Resource): resource to use
        """
        path_list, uri, path, args = request_data
        try:
            request._redirected
        except AttributeError:
            pass
        else:
            log.warning(D_(u"recursive redirection, please fix this URL:\n{old} ==> {new}").format(
                old=request.uri.decode('utf-8'),
                new=uri.decode('utf-8'),
                ))
            return web_resource.NoResource()
        log.debug(u"Redirecting URL {old} to {new}".format(
            old=request.uri.decode('utf-8'),
            new=uri.decode('utf-8'),
            ))
        # we change the request to reflect the new url
        request._redirected = True # here to avoid recursive redirections
        request.postpath = path_list[1:]
        request.uri = uri
        request.path = path
        request.args = args
        # and we start again to look for a child with the new url
        return self.getChildWithDefault(path_list[0], request)

    def getChildWithDefault(self, name, request):
        # XXX: this method is overriden only for root url
        #      which is the only ones who need to be handled before other children
        if name == '' and not request.postpath:
            return self._redirect(request, self.redirections[''])
        return super(LiberviaRootResource, self).getChildWithDefault(name, request)

    def getChild(self, name, request):
        resource = super(LiberviaRootResource, self).getChild(name, request)

        if isinstance(resource, web_resource.NoResource):
            # if nothing was found, we try our luck with redirections
            # XXX: we want redirections to happen only if everything else failed
            current_url = '/'.join([name] + request.postpath).lower()
            try:
                request_data = self.redirections[current_url]
            except KeyError:
                # no redirection for this url
                pass
            else:
                return self._redirect(request, request_data)

        return resource

    def createSimilarFile(self, path):
        # XXX: this method need to be overriden to avoid recreating a LiberviaRootResource

        f = LiberviaRootResource.__base__(path, self.defaultType, self.ignoredExts, self.registry)
        # refactoring by steps, here - constructor should almost certainly take these
        f.processors = self.processors
        f.indexNames = self.indexNames[:]
        f.childNotFound = self.childNotFound
        return f


class JSONRPCMethodManager(jsonrpc.JSONRPC):

    def __init__(self, sat_host):
        jsonrpc.JSONRPC.__init__(self)
        self.sat_host = sat_host

    def asyncBridgeCall(self, method_name, *args, **kwargs):
        return self.sat_host.bridgeCall(method_name, *args, **kwargs)


class MethodHandler(JSONRPCMethodManager):

    def __init__(self, sat_host):
        JSONRPCMethodManager.__init__(self, sat_host)

    def render(self, request):
        self.session = request.getSession()
        profile = session_iface.ISATSession(self.session).profile
        if not profile:
            #user is not identified, we return a jsonrpc fault
            parsed = jsonrpclib.loads(request.content.read())
            fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, C.NOT_ALLOWED)  # FIXME: define some standard error codes for libervia
            return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))  # pylint: disable=E1103
        return jsonrpc.JSONRPC.render(self, request)

    def jsonrpc_getVersion(self):
        """Return SàT version"""
        try:
            return self._version_cache
        except AttributeError:
            self._version_cache = self.sat_host.bridge.getVersion()
            return self._version_cache

    def jsonrpc_getLiberviaVersion(self):
        """Return Libervia version"""
        return self.sat_host.full_version

    def jsonrpc_disconnect(self):
        """Disconnect the profile"""
        sat_session = session_iface.ISATSession(self.session)
        profile = sat_session.profile
        self.sat_host.bridge.disconnect(profile)

    def jsonrpc_getContacts(self):
        """Return all passed args."""
        profile = session_iface.ISATSession(self.session).profile
        return self.sat_host.bridge.getContacts(profile)

    def jsonrpc_addContact(self, entity, name, groups):
        """Subscribe to contact presence, and add it to the given groups"""
        profile = session_iface.ISATSession(self.session).profile
        self.sat_host.bridge.addContact(entity, profile)
        self.sat_host.bridge.updateContact(entity, name, groups, profile)

    def jsonrpc_delContact(self, entity):
        """Remove contact from contacts list"""
        profile = session_iface.ISATSession(self.session).profile
        self.sat_host.bridge.delContact(entity, profile)

    def jsonrpc_updateContact(self, entity, name, groups):
        """Update contact's roster item"""
        profile = session_iface.ISATSession(self.session).profile
        self.sat_host.bridge.updateContact(entity, name, groups, profile)

    def jsonrpc_subscription(self, sub_type, entity):
        """Confirm (or infirm) subscription,
        and setup user roster in case of subscription"""
        profile = session_iface.ISATSession(self.session).profile
        self.sat_host.bridge.subscription(sub_type, entity, profile)

    def jsonrpc_getWaitingSub(self):
        """Return list of room already joined by user"""
        profile = session_iface.ISATSession(self.session).profile
        return self.sat_host.bridge.getWaitingSub(profile)

    def jsonrpc_setStatus(self, presence, status):
        """Change the presence and/or status
        @param presence: value from ("", "chat", "away", "dnd", "xa")
        @param status: any string to describe your status
        """
        profile = session_iface.ISATSession(self.session).profile
        self.sat_host.bridge.setPresence('', presence, {'': status}, profile)

    def jsonrpc_messageSend(self, to_jid, msg, subject, type_, extra={}):
        """send message"""
        profile = session_iface.ISATSession(self.session).profile
        return self.asyncBridgeCall("messageSend", to_jid, msg, subject, type_, extra, profile)

    ## PubSub ##

    def jsonrpc_psNodeDelete(self, service, node):
        """Delete a whole node

        @param service (unicode): service jid
        @param node (unicode): node to delete
        """
        profile = session_iface.ISATSession(self.session).profile
        return self.asyncBridgeCall("psNodeDelete", service, node, profile)

    # def jsonrpc_psRetractItem(self, service, node, item, notify):
    #     """Delete a whole node

    #     @param service (unicode): service jid
    #     @param node (unicode): node to delete
    #     @param items (iterable): id of item to retract
    #     @param notify (bool): True if notification is required
    #     """
    #     profile = session_iface.ISATSession(self.session).profile
    #     return self.asyncBridgeCall("psRetractItem", service, node, item, notify, profile)

    # def jsonrpc_psRetractItems(self, service, node, items, notify):
    #     """Delete a whole node

    #     @param service (unicode): service jid
    #     @param node (unicode): node to delete
    #     @param items (iterable): ids of items to retract
    #     @param notify (bool): True if notification is required
    #     """
    #     profile = session_iface.ISATSession(self.session).profile
    #     return self.asyncBridgeCall("psRetractItems", service, node, items, notify, profile)

    ## microblogging ##

    def jsonrpc_mbSend(self, service, node, mb_data):
        """Send microblog data

        @param service (unicode): service jid or empty string to use profile's microblog
        @param node (unicode): publishing node, or empty string to use microblog node
        @param mb_data(dict): microblog data
        @return: a deferred
        """
        profile = session_iface.ISATSession(self.session).profile
        return self.asyncBridgeCall("mbSend", service, node, mb_data, profile)

    def jsonrpc_mbRetract(self, service, node, items):
        """Delete a whole node

        @param service (unicode): service jid, empty string for PEP
        @param node (unicode): node to delete, empty string for default node
        @param items (iterable): ids of items to retract
        """
        profile = session_iface.ISATSession(self.session).profile
        return self.asyncBridgeCall("mbRetract", service, node, items, profile)

    def jsonrpc_mbGet(self, service_jid, node, max_items, item_ids, extra):
        """Get last microblogs from publisher_jid

        @param service_jid (unicode): pubsub service, usually publisher jid
        @param node(unicode): mblogs node, or empty string to get the defaut one
        @param max_items (int): maximum number of item to get or C.NO_LIMIT to get everything
        @param item_ids (list[unicode]): list of item IDs
        @param rsm (dict): TODO
        @return: a deferred couple with the list of items and metadatas.
        """
        profile = session_iface.ISATSession(self.session).profile
        return self.asyncBridgeCall("mbGet", service_jid, node, max_items, item_ids, extra, profile)

    def jsonrpc_mbGetFromMany(self, publishers_type, publishers, max_items, extra):
        """Get many blog nodes at once

        @param publishers_type (unicode): one of "ALL", "GROUP", "JID"
        @param publishers (tuple(unicode)): tuple of publishers (empty list for all, list of groups or list of jids)
        @param max_items (int): maximum number of item to get or C.NO_LIMIT to get everything
        @param extra (dict): TODO
        @return (str): RT Deferred session id
        """
        profile = session_iface.ISATSession(self.session).profile
        return self.sat_host.bridge.mbGetFromMany(publishers_type, publishers, max_items, extra, profile)

    def jsonrpc_mbGetFromManyRTResult(self, rt_session):
        """Get results from RealTime mbGetFromMany session

        @param rt_session (str): RT Deferred session id
        """
        profile = session_iface.ISATSession(self.session).profile
        return self.asyncBridgeCall("mbGetFromManyRTResult", rt_session, profile)

    def jsonrpc_mbGetFromManyWithComments(self, publishers_type, publishers, max_items, max_comments, rsm_dict, rsm_comments_dict):
        """Helper method to get the microblogs and their comments in one shot

        @param publishers_type (str): type of the list of publishers (one of "GROUP" or "JID" or "ALL")
        @param publishers (list): list of publishers, according to publishers_type (list of groups or list of jids)
        @param max_items (int): optional limit on the number of retrieved items.
        @param max_comments (int): maximum number of comments to retrieve
        @param rsm_dict (dict): RSM data for initial items only
        @param rsm_comments_dict (dict): RSM data for comments only
        @param profile_key: profile key
        @return (str): RT Deferred session id
        """
        profile = session_iface.ISATSession(self.session).profile
        return self.sat_host.bridge.mbGetFromManyWithComments(publishers_type, publishers, max_items, max_comments, rsm_dict, rsm_comments_dict, profile)

    def jsonrpc_mbGetFromManyWithCommentsRTResult(self, rt_session):
        """Get results from RealTime mbGetFromManyWithComments session

        @param rt_session (str): RT Deferred session id
        """
        profile = session_iface.ISATSession(self.session).profile
        return self.asyncBridgeCall("mbGetFromManyWithCommentsRTResult", rt_session, profile)


    # def jsonrpc_sendMblog(self, type_, dest, text, extra={}):
    #     """ Send microblog message
    #     @param type_ (unicode): one of "PUBLIC", "GROUP"
    #     @param dest (tuple(unicode)): recipient groups (ignored for "PUBLIC")
    #     @param text (unicode): microblog's text
    #     """
    #     profile = session_iface.ISATSession(self.session).profile
    #     extra['allow_comments'] = 'True'

    #     if not type_:  # auto-detect
    #         type_ = "PUBLIC" if dest == [] else "GROUP"

    #     if type_ in ("PUBLIC", "GROUP") and text:
    #         if type_ == "PUBLIC":
    #             #This text if for the public microblog
    #             log.debug("sending public blog")
    #             return self.sat_host.bridge.sendGroupBlog("PUBLIC", (), text, extra, profile)
    #         else:
    #             log.debug("sending group blog")
    #             dest = dest if isinstance(dest, list) else [dest]
    #             return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, profile)
    #     else:
    #         raise Exception("Invalid data")

    # def jsonrpc_deleteMblog(self, pub_data, comments):
    #     """Delete a microblog node
    #     @param pub_data: a tuple (service, comment node identifier, item identifier)
    #     @param comments: comments node identifier (for main item) or False
    #     """
    #     profile = session_iface.ISATSession(self.session).profile
    #     return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile)

    # def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}):
    #     """Modify a microblog node
    #     @param pub_data: a tuple (service, comment node identifier, item identifier)
    #     @param comments: comments node identifier (for main item) or False
    #     @param message: new message
    #     @param extra: dict which option name as key, which can be:
    #         - allow_comments: True to accept an other level of comments, False else (default: False)
    #         - rich: if present, contain rich text in currently selected syntax
    #     """
    #     profile = session_iface.ISATSession(self.session).profile
    #     if comments:
    #         extra['allow_comments'] = 'True'
    #     return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile)

    # def jsonrpc_sendMblogComment(self, node, text, extra={}):
    #     """ Send microblog message
    #     @param node: url of the comments node
    #     @param text: comment
    #     """
    #     profile = session_iface.ISATSession(self.session).profile
    #     if node and text:
    #         return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile)
    #     else:
    #         raise Exception("Invalid data")

    # def jsonrpc_getMblogs(self, publisher_jid, item_ids, max_items=C.RSM_MAX_ITEMS):
    #     """Get specified microblogs posted by a contact
    #     @param publisher_jid: jid of the publisher
    #     @param item_ids: list of microblogs items IDs
    #     @return list of microblog data (dict)"""
    #     profile = session_iface.ISATSession(self.session).profile
    #     d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, {'max_': unicode(max_items)}, False, profile)
    #     return d

    # def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids, max_comments=C.RSM_MAX_COMMENTS):
    #     """Get specified microblogs posted by a contact and their comments
    #     @param publisher_jid: jid of the publisher
    #     @param item_ids: list of microblogs items IDs
    #     @return list of couple (microblog data, list of microblog data)"""
    #     profile = session_iface.ISATSession(self.session).profile
    #     d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, {}, max_comments, profile)
    #     return d

    # def jsonrpc_getMassiveMblogs(self, publishers_type, publishers, rsm=None):
    #     """Get lasts microblogs posted by several contacts at once

    #     @param publishers_type (unicode): one of "ALL", "GROUP", "JID"
    #     @param publishers (tuple(unicode)): tuple of publishers (empty list for all, list of groups or list of jids)
    #     @param rsm (dict): TODO
    #     @return: dict{unicode: list[dict])
    #         key: publisher's jid
    #         value: list of microblog data (dict)
    #     """
    #     profile = session_iface.ISATSession(self.session).profile
    #     if rsm is None:
    #         rsm = {'max_': unicode(C.RSM_MAX_ITEMS)}
    #     d = self.asyncBridgeCall("getMassiveGroupBlogs", publishers_type, publishers, rsm, profile)
    #     self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers, profile)
    #     return d

    # def jsonrpc_getMblogComments(self, service, node, rsm=None):
    #     """Get all comments of given node
    #     @param service: jid of the service hosting the node
    #     @param node: comments node
    #     """
    #     profile = session_iface.ISATSession(self.session).profile
    #     if rsm is None:
    #         rsm = {'max_': unicode(C.RSM_MAX_COMMENTS)}
    #     d = self.asyncBridgeCall("getGroupBlogComments", service, node, rsm, profile)
    #     return d

    def jsonrpc_getPresenceStatuses(self):
        """Get Presence information for connected contacts"""
        profile = session_iface.ISATSession(self.session).profile
        return self.sat_host.bridge.getPresenceStatuses(profile)

    def jsonrpc_historyGet(self, from_jid, to_jid, size, between, search=''):
        """Return history for the from_jid/to_jid couple"""
        sat_session = session_iface.ISATSession(self.session)
        profile = sat_session.profile
        sat_jid = sat_session.jid
        if not sat_jid:
            # we keep a session cache for jid to avoir jid spoofing
            sat_jid = sat_session.jid = jid.JID(self.sat_host.bridge.getParamA("JabberID", "Connection", profile_key=profile))
        if jid.JID(from_jid).userhost() != sat_jid.userhost() and jid.JID(to_jid).userhost() != sat_jid.userhost():
            log.error(u"Trying to get history from a different jid (given (browser): {}, real (backend): {}), maybe a hack attempt ?".format(from_jid, sat_jid))
            return {}
        d = self.asyncBridgeCall("historyGet", from_jid, to_jid, size, between, search, profile)

        def show(result_dbus):
            result = []
            for line in result_dbus:
                #XXX: we have to do this stupid thing because Python D-Bus use its own types instead of standard types
                #     and txJsonRPC doesn't accept D-Bus types, resulting in a empty query
                uuid, timestamp, from_jid, to_jid, message, subject, mess_type, extra = line
                result.append((unicode(uuid), float(timestamp), unicode(from_jid), unicode(to_jid), dict(message), dict(subject), unicode(mess_type), dict(extra)))
            return result
        d.addCallback(show)
        return d

    def jsonrpc_mucJoin(self, room_jid, nick):
        """Join a Multi-User Chat room

        @param room_jid (unicode): room JID or empty string to generate a unique name
        @param nick (unicode): user nick
        """
        profile = session_iface.ISATSession(self.session).profile
        d = self.asyncBridgeCall("joinMUC", room_jid, nick, {}, profile)
        return d

    def jsonrpc_inviteMUC(self, contact_jid, room_jid):
        """Invite a user to a Multi-User Chat room

        @param contact_jid (unicode): contact to invite
        @param room_jid (unicode): room JID or empty string to generate a unique name
        """
        profile = session_iface.ISATSession(self.session).profile
        room_id = room_jid.split("@")[0]
        service = room_jid.split("@")[1]
        self.sat_host.bridge.inviteMUC(contact_jid, service, room_id, {}, profile)

    def jsonrpc_mucLeave(self, room_jid):
        """Quit a Multi-User Chat room"""
        profile = session_iface.ISATSession(self.session).profile
        try:
            room_jid = jid.JID(room_jid)
        except:
            log.warning('Invalid room jid')
            return
        self.sat_host.bridge.mucLeave(room_jid.userhost(), profile)

    def jsonrpc_mucGetRoomsJoined(self):
        """Return list of room already joined by user"""
        profile = session_iface.ISATSession(self.session).profile
        return self.sat_host.bridge.mucGetRoomsJoined(profile)

    def jsonrpc_mucGetDefaultService(self):
        """@return: the default MUC"""
        d = self.asyncBridgeCall("mucGetDefaultService")
        return d

    def jsonrpc_launchTarotGame(self, other_players, room_jid=""):
        """Create a room, invite the other players and start a Tarot game.

        @param other_players (list[unicode]): JIDs of the players to play with
        @param room_jid (unicode): room JID or empty string to generate a unique name
        """
        profile = session_iface.ISATSession(self.session).profile
        self.sat_host.bridge.tarotGameLaunch(other_players, room_jid, profile)

    def jsonrpc_getTarotCardsPaths(self):
        """Give the path of all the tarot cards"""
        _join = os.path.join
        _media_dir = _join(self.sat_host.media_dir, '')
        return map(lambda x: _join(C.MEDIA_DIR, x[len(_media_dir):]), glob.glob(_join(_media_dir, C.CARDS_DIR, '*_*.png')))

    def jsonrpc_tarotGameReady(self, player, referee):
        """Tell to the server that we are ready to start the game"""
        profile = session_iface.ISATSession(self.session).profile
        self.sat_host.bridge.tarotGameReady(player, referee, profile)

    def jsonrpc_tarotGamePlayCards(self, player_nick, referee, cards):
        """Tell to the server the cards we want to put on the table"""
        profile = session_iface.ISATSession(self.session).profile
        self.sat_host.bridge.tarotGamePlayCards(player_nick, referee, cards, profile)

    def jsonrpc_launchRadioCollective(self, invited, room_jid=""):
        """Create a room, invite people, and start a radio collective.

        @param invited (list[unicode]): JIDs of the contacts to play with
        @param room_jid (unicode): room JID or empty string to generate a unique name
        """
        profile = session_iface.ISATSession(self.session).profile
        self.sat_host.bridge.radiocolLaunch(invited, room_jid, profile)

    def jsonrpc_getEntitiesData(self, jids, keys):
        """Get cached data for several entities at once

        @param jids: list jids from who we wants data, or empty list for all jids in cache
        @param keys: name of data we want (list)
        @return: requested data"""
        if not C.ALLOWED_ENTITY_DATA.issuperset(keys):
            raise exceptions.PermissionError("Trying to access unallowed data (hack attempt ?)")
        profile = session_iface.ISATSession(self.session).profile
        try:
            return self.sat_host.bridge.getEntitiesData(jids, keys, profile)
        except Exception as e:
            raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e)))

    def jsonrpc_getEntityData(self, jid, keys):
        """Get cached data for an entity

        @param jid: jid of contact from who we want data
        @param keys: name of data we want (list)
        @return: requested data"""
        if not C.ALLOWED_ENTITY_DATA.issuperset(keys):
            raise exceptions.PermissionError("Trying to access unallowed data (hack attempt ?)")
        profile = session_iface.ISATSession(self.session).profile
        try:
            return self.sat_host.bridge.getEntityData(jid, keys, profile)
        except Exception as e:
            raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e)))

    def jsonrpc_getCard(self, jid_):
        """Get VCard for entiry
        @param jid_: jid of contact from who we want data
        @return: id to retrieve the profile"""
        profile = session_iface.ISATSession(self.session).profile
        return self.sat_host.bridge.getCard(jid_, profile)

    @defer.inlineCallbacks
    def jsonrpc_avatarGet(self, entity, cache_only, hash_only):
        session_data = session_iface.ISATSession(self.session)
        profile = session_data.profile
        # profile_uuid = session_data.uuid
        avatar = yield self.asyncBridgeCall("avatarGet", entity, cache_only, hash_only, profile)
        if hash_only:
            defer.returnValue(avatar)
        else:
            filename = os.path.basename(avatar)
            avatar_url = os.path.join(C.CACHE_DIR, session_data.uuid, filename)
            defer.returnValue(avatar_url)

    def jsonrpc_getAccountDialogUI(self):
        """Get the dialog for managing user account
        @return: XML string of the XMLUI"""
        profile = session_iface.ISATSession(self.session).profile
        return self.sat_host.bridge.getAccountDialogUI(profile)

    def jsonrpc_getParamsUI(self):
        """Return the parameters XML for profile"""
        profile = session_iface.ISATSession(self.session).profile
        return self.asyncBridgeCall("getParamsUI", C.SECURITY_LIMIT, C.APP_NAME, profile)

    def jsonrpc_asyncGetParamA(self, param, category, attribute="value"):
        """Return the parameter value for profile"""
        profile = session_iface.ISATSession(self.session).profile
        if category == "Connection":
            # we need to manage the followings params here, else SECURITY_LIMIT would block them
            if param == "JabberID":
                return self.asyncBridgeCall("asyncGetParamA", param, category, attribute, profile_key=profile)
            elif param == "autoconnect":
                return defer.succeed(C.BOOL_TRUE)
        d = self.asyncBridgeCall("asyncGetParamA", param, category, attribute, C.SECURITY_LIMIT, profile_key=profile)
        return d

    def jsonrpc_setParam(self, name, value, category):
        profile = session_iface.ISATSession(self.session).profile
        return self.sat_host.bridge.setParam(name, value, category, C.SECURITY_LIMIT, profile)

    def jsonrpc_launchAction(self, callback_id, data):
        #FIXME: any action can be launched, this can be a huge security issue if callback_id can be guessed
        #       a security system with authorised callback_id must be implemented, similar to the one for authorised params
        profile = session_iface.ISATSession(self.session).profile
        d = self.asyncBridgeCall("launchAction", callback_id, data, profile)
        return d

    def jsonrpc_chatStateComposing(self, to_jid_s):
        """Call the method to process a "composing" state.
        @param to_jid_s: contact the user is composing to
        """
        profile = session_iface.ISATSession(self.session).profile
        self.sat_host.bridge.chatStateComposing(to_jid_s, profile)

    def jsonrpc_getNewAccountDomain(self):
        """@return: the domain for new account creation"""
        d = self.asyncBridgeCall("getNewAccountDomain")
        return d

    def jsonrpc_syntaxConvert(self, text, syntax_from=C.SYNTAX_XHTML, syntax_to=C.SYNTAX_CURRENT):
        """ Convert a text between two syntaxes
        @param text: text to convert
        @param syntax_from: source syntax (e.g. "markdown")
        @param syntax_to: dest syntax (e.g.: "XHTML")
        @param safe: clean resulting XHTML to avoid malicious code if True (forced here)
        @return: converted text """
        profile = session_iface.ISATSession(self.session).profile
        return self.sat_host.bridge.syntaxConvert(text, syntax_from, syntax_to, True, profile)

    def jsonrpc_getLastResource(self, jid_s):
        """Get the last active resource of that contact."""
        profile = session_iface.ISATSession(self.session).profile
        return self.sat_host.bridge.getLastResource(jid_s, profile)

    def jsonrpc_getFeatures(self):
        """Return the available features in the backend for profile"""
        profile = session_iface.ISATSession(self.session).profile
        return self.sat_host.bridge.getFeatures(profile)

    def jsonrpc_skipOTR(self):
        """Tell the backend to leave OTR handling to Libervia."""
        profile = session_iface.ISATSession(self.session).profile
        return self.sat_host.bridge.skipOTR(profile)


class WaitingRequests(dict):

    def setRequest(self, request, profile, register_with_ext_jid=False):
        """Add the given profile to the waiting list.

        @param request (server.Request): the connection request
        @param profile (str): %(doc_profile)s
        @param register_with_ext_jid (bool): True if we will try to register the profile with an external XMPP account credentials
        """
        dc = reactor.callLater(BRIDGE_TIMEOUT, self.purgeRequest, profile)
        self[profile] = (request, dc, register_with_ext_jid)

    def purgeRequest(self, profile):
        """Remove the given profile from the waiting list.

        @param profile (str): %(doc_profile)s
        """
        try:
            dc = self[profile][1]
        except KeyError:
            return
        if dc.active():
            dc.cancel()
        del self[profile]

    def getRequest(self, profile):
        """Get the waiting request for the given profile.

        @param profile (str): %(doc_profile)s
        @return: the waiting request or None
        """
        return self[profile][0] if profile in self else None

    def getRegisterWithExtJid(self, profile):
        """Get the value of the register_with_ext_jid parameter.

        @param profile (str): %(doc_profile)s
        @return: bool or None
        """
        return self[profile][2] if profile in self else None


class Register(JSONRPCMethodManager):
    """This class manage the registration procedure with SàT
    It provide an api for the browser, check password and setup the web server"""

    def __init__(self, sat_host):
        JSONRPCMethodManager.__init__(self, sat_host)
        self.profiles_waiting = {}
        self.request = None

    def render(self, request):
        """
        Render method with some hacks:
           - if login is requested, try to login with form data
           - except login, every method is jsonrpc
           - user doesn't need to be authentified for explicitely listed methods, but must be for all others
        """
        if request.postpath == ['login']:
            return self.loginOrRegister(request)
        _session = request.getSession()
        parsed = jsonrpclib.loads(request.content.read())
        method = parsed.get("method")  # pylint: disable=E1103
        if  method not in ['getSessionMetadata', 'registerParams', 'menusGet']:
            #if we don't call these methods, we need to be identified
            profile = session_iface.ISATSession(_session).profile
            if not profile:
                #user is not identified, we return a jsonrpc fault
                fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, C.NOT_ALLOWED)  # FIXME: define some standard error codes for libervia
                return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))  # pylint: disable=E1103
        self.request = request
        return jsonrpc.JSONRPC.render(self, request)

    def loginOrRegister(self, request):
        """This method is called with the POST information from the registering form.

        @param request: request of the register form
        @return: a constant indicating the state:
            - C.BAD_REQUEST: something is wrong in the request (bad arguments)
            - a return value from self._loginAccount or self._registerNewAccount
        """
        try:
            submit_type = request.args['submit_type'][0]
        except KeyError:
            return C.BAD_REQUEST

        if submit_type == 'register':
            self._registerNewAccount(request)
            return server.NOT_DONE_YET
        elif submit_type == 'login':
            self._loginAccount(request)
            return server.NOT_DONE_YET
        return Exception('Unknown submit type')

    @defer.inlineCallbacks
    def _registerNewAccount(self, request):
        try:
            login = request.args['register_login'][0]
            password = request.args['register_password'][0]
            email = request.args['email'][0]
        except KeyError:
            request.write(C.BAD_REQUEST)
            request.finish()
            return
        status = yield self.sat_host.registerNewAccount(request, login, password, email)
        request.write(status)
        request.finish()

    @defer.inlineCallbacks
    def _loginAccount(self, request):
        """Try to authenticate the user with the request information.

        will write to request a constant indicating the state:
            - C.PROFILE_LOGGED: profile is connected
            - C.PROFILE_LOGGED_EXT_JID: profile is connected and an external jid has been used
            - C.SESSION_ACTIVE: session was already active
            - C.BAD_REQUEST: something is wrong in the request (bad arguments)
            - C.PROFILE_AUTH_ERROR: either the profile (login) or the profile password is wrong
            - C.XMPP_AUTH_ERROR: the profile is authenticated but the XMPP password is wrong
            - C.ALREADY_WAITING: a request has already been submitted for this profil, C.PROFILE_LOGGED_EXT_JID)e
            - C.NOT_CONNECTED: connection has not been established
        the request will then be finished
        @param request: request of the register form
        """
        try:
            login = request.args['login'][0]
            password = request.args['login_password'][0]
        except KeyError:
            request.write(C.BAD_REQUEST)
            request.finish()
            return

        assert login

        try:
            status = yield self.sat_host.connect(request, login, password)
        except (exceptions.DataError,
                exceptions.ProfileUnknownError,
                exceptions.PermissionError):
            request.write(C.PROFILE_AUTH_ERROR)
            request.finish()
            return
        except exceptions.NotReady:
            request.write(C.ALREADY_WAITING)
            request.finish()
            return
        except exceptions.TimeOutError:
            request.write(C.NO_REPLY)
            request.finish()
            return
        except exceptions.InternalError as e:
            request.write(e.message)
            request.finish()
            return
        except exceptions.ConflictError:
            request.write(C.SESSION_ACTIVE)
            request.finish()
            return
        except ValueError as e:
            if e.message in (C.PROFILE_AUTH_ERROR, C.XMPP_AUTH_ERROR):
                request.write(e.message)
                request.finish()
                return
            else:
                raise e

        assert status
        request.write(status)
        request.finish()

    def jsonrpc_isConnected(self):
        _session = self.request.getSession()
        profile = session_iface.ISATSession(_session).profile
        return self.sat_host.bridge.isConnected(profile)

    def jsonrpc_connect(self):
        _session = self.request.getSession()
        profile = session_iface.ISATSession(_session).profile
        if self.waiting_profiles.getRequest(profile):
            raise jsonrpclib.Fault(1, C.ALREADY_WAITING)  # FIXME: define some standard error codes for libervia
        self.waiting_profiles.setRequest(self.request, profile)
        self.sat_host.bridge.connect(profile)
        return server.NOT_DONE_YET

    def jsonrpc_getSessionMetadata(self):
        """Return metadata useful on session start

        @return (dict): metadata which can have the following keys:
            "plugged" (bool): True if a profile is already plugged
            "warning" (unicode): a security warning message if plugged is False and if it make sense
                this key may not be present
            "allow_registration" (bool): True if registration is allowed
                this key is only present if profile is unplugged
        @return: a couple (registered, message) with:
        - registered:
        - message:
        """
        metadata = {}
        _session = self.request.getSession()
        profile = session_iface.ISATSession(_session).profile
        if profile:
            metadata["plugged"] = True
        else:
            metadata["plugged"] = False
            metadata["warning"] = self._getSecurityWarning()
            metadata["allow_registration"] = self.sat_host.options["allow_registration"]
        return metadata

    def jsonrpc_registerParams(self):
        """Register the frontend specific parameters"""
        # params = """<params><individual>...</category></individual>"""
        # self.sat_host.bridge.paramsRegisterApp(params, C.SECURITY_LIMIT, C.APP_NAME)

    def jsonrpc_menusGet(self):
        """Return the parameters XML for profile"""
        # XXX: we put this method in Register because we get menus before being logged
        return self.sat_host.bridge.menusGet('', C.SECURITY_LIMIT)

    def _getSecurityWarning(self):
        """@return: a security warning message, or None if the connection is secure"""
        if self.request.URLPath().scheme == 'https' or not self.sat_host.options['security_warning']:
            return None
        text = "<p>" + D_("You are about to connect to an unsecure service.") + "</p><p>&nbsp;</p><p>"

        if self.sat_host.options['connection_type'] == 'both':
            new_port = (':%s' % self.sat_host.options['port_https_ext']) if self.sat_host.options['port_https_ext'] != HTTPS_PORT else ''
            url = "https://%s" % self.request.URLPath().netloc.replace(':%s' % self.sat_host.options['port'], new_port)
            text += D_('Please read our %(faq_prefix)ssecurity notice%(faq_suffix)s regarding HTTPS') % {'faq_prefix': '<a href="http://salut-a-toi.org/faq.html#https" target="#">', 'faq_suffix': '</a>'}
            text += "</p><p>" + D_('and use the secure version of this website:')
            text += '</p><p>&nbsp;</p><p align="center"><a href="%(url)s">%(url)s</a>' % {'url': url}
        else:
            text += D_('You should ask your administrator to turn on HTTPS.')

        return text + "</p><p>&nbsp;</p>"


class SignalHandler(jsonrpc.JSONRPC):

    def __init__(self, sat_host):
        web_resource.Resource.__init__(self)
        self.register = None
        self.sat_host = sat_host
        self.signalDeferred = {} # dict of deferred (key: profile, value: Deferred)
                                 # which manages the long polling HTTP request with signals
        self.queue = {}

    def plugRegister(self, register):
        self.register = register

    def jsonrpc_getSignals(self):
        """Keep the connection alive until a signal is received, then send it
        @return: (signal, *signal_args)"""
        _session = self.request.getSession()
        profile = session_iface.ISATSession(_session).profile
        if profile in self.queue:  # if we have signals to send in queue
            if self.queue[profile]:
                return self.queue[profile].pop(0)
            else:
                #the queue is empty, we delete the profile from queue
                del self.queue[profile]
        _session.lock()  # we don't want the session to expire as long as this connection is active

        def unlock(signal, profile):
            _session.unlock()
            try:
                source_defer = self.signalDeferred[profile]
                if source_defer.called and source_defer.result[0] == "disconnected":
                    log.info(u"[%s] disconnected" % (profile,))
                    try:
                        _session.expire()
                    except KeyError:
                        # FIXME: happen if session is ended using login page
                        #        when pyjamas page is also launched
                        log.warning(u'session is already expired')
            except IndexError:
                log.error("Deferred result should be a tuple with fonction name first")

        self.signalDeferred[profile] = defer.Deferred()
        self.request.notifyFinish().addBoth(unlock, profile)
        return self.signalDeferred[profile]

    def getGenericCb(self, function_name):
        """Return a generic function which send all params to signalDeferred.callback
        function must have profile as last argument"""
        def genericCb(*args):
            profile = args[-1]
            if not profile in self.sat_host.prof_connected:
                return
            signal_data = (function_name, args[:-1])
            try:
                signal_callback = self.signalDeferred[profile].callback
            except KeyError:
                self.queue.setdefault(profile,[]).append(signal_data)
            else:
                signal_callback(signal_data)
                del self.signalDeferred[profile]
        return genericCb

    def actionNewHandler(self, action_data, action_id, security_limit, profile):
        """actionNew handler

        XXX: We need need a dedicated handler has actionNew use a security_limit which must be managed
        @param action_data(dict): see bridge documentation
        @param action_id(unicode): identitifer of the action
        @param security_limit(int): %(doc_security_limit)s
        @param profile(unicode): %(doc_profile)s
        """
        if not profile in self.sat_host.prof_connected:
            return
        # FIXME: manage security limit in a dedicated method
        #        raise an exception if it's not OK
        #        and read value in sat.conf
        if security_limit >= C.SECURITY_LIMIT:
            log.debug(u"Ignoring action  {action_id}, blocked by security limit".format(action_id=action_id))
            return
        signal_data = ("actionNew", (action_data, action_id, security_limit))
        try:
            signal_callback = self.signalDeferred[profile].callback
        except KeyError:
            self.queue.setdefault(profile,[]).append(signal_data)
        else:
            signal_callback(signal_data)
            del self.signalDeferred[profile]

    def connected(self, profile, jid_s):
        """Connection is done.

        @param profile (unicode): %(doc_profile)s
        @param jid_s (unicode): the JID that we were assigned by the server, as the resource might differ from the JID we asked for.
        """
        #  FIXME: _logged should not be called from here, check this code
        #  FIXME: check if needed to connect with external jid
        # jid_s is handled in QuickApp.connectionHandler already
        # assert self.register  # register must be plugged
        # request = self.sat_host.waiting_profiles.getRequest(profile)
        # if request:
        #     self.sat_host._logged(profile, request)

    def disconnected(self, profile):
        if not profile in self.sat_host.prof_connected:
            log.error("'disconnected' signal received for a not connected profile")
            return
        self.sat_host.prof_connected.remove(profile)
        if profile in self.signalDeferred:
            self.signalDeferred[profile].callback(("disconnected",))
            del self.signalDeferred[profile]
        else:
            if profile not in self.queue:
                self.queue[profile] = []
            self.queue[profile].append(("disconnected",))

    def render(self, request):
        """
        Render method wich reject access if user is not identified
        """
        _session = request.getSession()
        parsed = jsonrpclib.loads(request.content.read())
        profile = session_iface.ISATSession(_session).profile
        if not profile:
            #user is not identified, we return a jsonrpc fault
            fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, C.NOT_ALLOWED)  # FIXME: define some standard error codes for libervia
            return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))  # pylint: disable=E1103
        self.request = request
        return jsonrpc.JSONRPC.render(self, request)


class UploadManager(web_resource.Resource):
    """This class manage the upload of a file
    It redirect the stream to SàT core backend"""
    isLeaf = True
    NAME = 'path'  # name use by the FileUpload

    def __init__(self, sat_host):
        self.sat_host = sat_host
        self.upload_dir = tempfile.mkdtemp()
        self.sat_host.addCleanup(shutil.rmtree, self.upload_dir)

    def getTmpDir(self):
        return self.upload_dir

    def _getFileName(self, request):
        """Generate unique filename for a file"""
        raise NotImplementedError

    def _fileWritten(self, request, filepath):
        """Called once the file is actually written on disk
        @param request: HTTP request object
        @param filepath: full filepath on the server
        @return: a tuple with the name of the async bridge method
        to be called followed by its arguments.
        """
        raise NotImplementedError

    def render(self, request):
        """
        Render method with some hacks:
           - if login is requested, try to login with form data
           - except login, every method is jsonrpc
           - user doesn't need to be authentified for getSessionMetadata, but must be for all other methods
        """
        filename = self._getFileName(request)
        filepath = os.path.join(self.upload_dir, filename)
        #FIXME: the uploaded file is fully loaded in memory at form parsing time so far
        #       (see twisted.web.http.Request.requestReceived). A custom requestReceived should
        #       be written in the futur. In addition, it is not yet possible to get progression informations
        #       (see http://twistedmatrix.com/trac/ticket/288)

        with open(filepath, 'w') as f:
            f.write(request.args[self.NAME][0])

        def finish(d):
            error = isinstance(d, Exception) or isinstance(d, failure.Failure)
            request.write(C.UPLOAD_KO if error else C.UPLOAD_OK)
            # TODO: would be great to re-use the original Exception class and message
            # but it is lost in the middle of the backtrace and encapsulated within
            # a DBusException instance --> extract the data from the backtrace?
            request.finish()

        d = JSONRPCMethodManager(self.sat_host).asyncBridgeCall(*self._fileWritten(request, filepath))
        d.addCallbacks(lambda d: finish(d), lambda failure: finish(failure))
        return server.NOT_DONE_YET


class UploadManagerRadioCol(UploadManager):
    NAME = 'song'

    def _getFileName(self, request):
        extension = os.path.splitext(request.args['filename'][0])[1]
        return "%s%s" % (str(uuid.uuid4()), extension)  # XXX: chromium doesn't seem to play song without the .ogg extension, even with audio/ogg mime-type

    def _fileWritten(self, request, filepath):
        """Called once the file is actually written on disk
        @param request: HTTP request object
        @param filepath: full filepath on the server
        @return: a tuple with the name of the async bridge method
        to be called followed by its arguments.
        """
        profile = session_iface.ISATSession(request.getSession()).profile
        return ("radiocolSongAdded", request.args['referee'][0], filepath, profile)


class UploadManagerAvatar(UploadManager):
    NAME = 'avatar_path'

    def _getFileName(self, request):
        return str(uuid.uuid4())

    def _fileWritten(self, request, filepath):
        """Called once the file is actually written on disk
        @param request: HTTP request object
        @param filepath: full filepath on the server
        @return: a tuple with the name of the async bridge method
        to be called followed by its arguments.
        """
        profile = session_iface.ISATSession(request.getSession()).profile
        return ("setAvatar", filepath, profile)


class LiberviaPage(web_resource.Resource):
    isLeaf = True  # we handle subpages ourself
    named_pages = {}
    uri_callbacks = {}

    def __init__(self, host, root_dir, name=None, redirect=None, access=None, parse_url=None,
                 prepare_render=None, render=None, template=None, on_data_post=None):
        """initiate LiberviaPages

        LiberviaPages are the main resources of Libervia, using easy to set python files
        The arguments are the variables found in page_meta.py
        @param host(Libervia): the running instance of Libervia
        @param root_dir(unicode): aboslute path of the page
        @param name(unicode, None): if not None, a unique name to identify the page
            can then be used for e.g. redirection
            "/" is not allowed in names (as it can be used to construct URL paths)
        @param redirect(unicode, None): if not None, this page will be a redirected
            parameter is used as in self.pageRedirect. parse_url will not be skipped
            using this redirect parameter is called "full redirection"
            using self.pageRedirect is called "partial redirection" (because some rendering method
            can still be used, e.g. parse_url)
        @param access(unicode, None): permission needed to access the page
            None means public access.
            Pages inherit from parent pages: e.g. if a "settings" page is restricted to admins,
            and if "settings/blog" is public, it still can only be accessed by admins.
            see C.PAGES_ACCESS_* for details
        @param parse_url(callable, None): if set it will be called to handle the URL path
            after this method, the page will be rendered if noting is left in path (request.postpath)
            else a the request will be transmitted to a subpage
        @param prepare_render(callable, None): if set, will be used to prepare the rendering
            that often means gathering data using the bridge
        @param render(callable, None): if not template is set, this method will be called and
            what it returns will be rendered.
            This method is mutually exclusive with template and must return a unicode string.
        @param template(unicode, None): path to the template to render.
            This method is mutually exclusive with render
        @param on_data_post(callable, None): method to call when data is posted
            None if not post is handled
        """

        web_resource.Resource.__init__(self)
        self.host = host
        self.root_dir = root_dir
        if name is not None:
            if name in self.named_pages:
                raise exceptions.ConflictError(_(u'a Libervia page named "{}" already exists'.format(name)))
            if u'/' in name:
                raise ValueError(_(u'"/" is not allowed in page names'))
            if not name:
                raise ValueError(_(u"a page name can't be empty"))
            self.named_pages[name] = self
        if access is None:
            access = C.PAGES_ACCESS_PUBLIC
        if access not in (C.PAGES_ACCESS_PUBLIC, C.PAGES_ACCESS_PROFILE, C.PAGES_ACCESS_NONE):
            raise NotImplementedError(_(u"{} access is not implemented yet").format(access))
        self.access = access
        if redirect is not None:
            # only page access and name make sense in case of full redirection
            if not all(lambda x: x is not None
                for x in (parse_url, prepare_render, render, template)):
                    raise ValueError(_(u"you can't use full page redirection with other rendering method,"
                                       u"check self.pageRedirect if you need to use them"))
            self.redirect = redirect
        else:
            self.redirect = None
        self.parse_url = parse_url
        self.prepare_render = prepare_render
        self.template = template
        self.render_method = render
        self.on_data_post = on_data_post
        if access == C.PAGES_ACCESS_NONE:
            # none pages just return a 404, no further check is needed
            return
        if template is None:
            if not callable(render):
                log.error(_(u"render must be implemented and callable if template is not set"))
        else:
            if render is not None:
                log.error(_(u"render can't be used at the same time as template"))
        if parse_url is not None and not callable(parse_url):
            log.error(_(u"parse_url must be a callable"))

    @classmethod
    def importPages(cls, host, parent=None, path=None):
        """Recursively import Libervia pages"""
        if path is None:
            path = []
        if parent is None:
            root_dir = os.path.join(os.path.dirname(libervia.__file__), C.PAGES_DIR)
            parent = host
        else:
            root_dir = parent.root_dir
        for d in os.listdir(root_dir):
            dir_path = os.path.join(root_dir, d)
            if not os.path.isdir(dir_path):
                continue
            meta_path = os.path.join(dir_path, C.PAGES_META_FILE)
            if os.path.isfile(meta_path):
                page_data = {}
                # we don't want to force the presence of __init__.py
                # so we use execfile instead of import.
                # TODO: when moved to Python 3, __init__.py is not mandatory anymore
                #       so we can switch to import
                execfile(meta_path, page_data)
                resource = LiberviaPage(
                    host,
                    dir_path,
                    name=page_data.get('name'),
                    redirect=page_data.get('redirect'),
                    access=page_data.get('access'),
                    parse_url=page_data.get('parse_url'),
                    prepare_render=page_data.get('prepare_render'),
                    render=page_data.get('render'),
                    template=page_data.get('template'),
                    on_data_post=page_data.get('on_data_post'))
                parent.putChild(d, resource)
                new_path = path + [d]
                log.info(u"Added /{path} page".format(path=u'[...]/'.join(new_path)))
                if 'uri_handlers' in page_data:
                    if not isinstance(page_data, dict):
                        log.error(_(u'uri_handlers must be a dict'))
                    else:
                        for uri_tuple, cb_name in page_data['uri_handlers'].iteritems():
                            if len(uri_tuple) != 2 or not isinstance(cb_name, basestring):
                                log.error(_(u"invalid uri_tuple"))
                                continue
                            log.info(_(u'setting {}/{} URIs handler').format(*uri_tuple))
                            try:
                                cb = page_data[cb_name]
                            except KeyError:
                                log.error(_(u'missing {name} method to handle {1}/{2}').format(
                                    name = cb_name, *uri_tuple))
                                continue
                            else:
                                cls.registerURI(uri_tuple, cb, new_path)

                LiberviaPage.importPages(host, resource, new_path)

    @classmethod
    def registerURI(cls, uri_tuple, get_uri_cb, pre_path):
        """register a URI handler

        @param uri_tuple(tuple[unicode, unicode]): type or URIs handler
            type/subtype as returned by tools/common/parseXMPPUri
        @param get_uri_cb(callable): method which take uri_data dict as only argument
            and return path with correct arguments relative to page itself
        @param pre_path(list[unicode]): prefix path to reference the handler page
        """
        if uri_tuple in cls.uri_callbacks:
            log.info(_(u"{}/{} URIs are already handled, replacing by the new handler").format(*uri_tuple))
        cls.uri_callbacks[uri_tuple] = {u'callback': get_uri_cb,
                                        u'pre_path': pre_path}

    def getPagePathFromURI(self, uri):
        """Retrieve page URL from xmpp: URI

        @param uri(unicode): URI with a xmpp: scheme
        @return (unicode,None): absolute path (starting from root "/") to page handling the URI
            None is returned if not page has been registered for this URI
        """
        uri_data = common_uri.parseXMPPUri(uri)
        try:
            callback_data = self.uri_callbacks[uri_data['type'], uri_data.get('sub_type')]
        except KeyError:
            return
        else:
            url = os.path.join(u'/', u'/'.join(callback_data['pre_path']), callback_data['callback'](self, uri_data))
        return url

    def getPageByName(self, name):
        """retrieve page instance from its name

        @param name(unicode): name of the page
        @return (LiberviaPage): page instance
        @raise KeyError: the page doesn't exist
        """
        return self.named_pages[name]

    def getChildWithDefault(self, path, request):
        # we handle children ourselves
        raise exceptions.InternalError(u"this method should not be used with LiberviaPage")

    def nextPath(self, request):
        """get next URL path segment, and update request accordingly

        will move first segment of postpath in prepath
        @param request(server.Request): current HTTP request
        @return (unicode): unquoted segment
        @raise IndexError: there is no segment left
        """
        pathElement = request.postpath.pop(0)
        request.prepath.append(pathElement)
        return urllib.unquote(pathElement).decode('utf-8')

    def pageRedirect(self, page_path, request, skip_parse_url=True):
        """redirect a page to a named page

        the workflow will continue with the workflow of the named page,
        skipping named page's parse_url method if it exist.
        @param page_path(unicode): path to page (elements are separated by "/"):
            if path starts with a "/":
                path is a full path starting from root
            else:
                - first element is name as registered in name variable
                - following element are subpages path
            e.g.: "blog" redirect to page named "blog"
                  "blog/atom.xml" redirect to atom.xml subpage of "blog"
                  "/common/blog/atom.xml" redirect to the page at the fiven full path
        @param request(server.Request): current HTTP request
        @param skip_parse_url(bool): if True, parse_url method on redirect page will be skipped
        @raise KeyError: there is no known page with this name
        """
        # FIXME: render non LiberviaPage resources
        path = page_path.rstrip(u'/').split(u'/')
        if not path[0]:
            redirect_page = self.host.root
        else:
            redirect_page = self.named_pages[path[0]]

        for subpage in path[1:]:
            redirect_page = redirect_page.childen[subpage]

        redirect_page.renderPage(request, skip_parse_url=True)
        raise failure.Failure(exceptions.CancelError(u'page redirection is used'))

    def pageError(self, request, code=C.HTTP_NOT_FOUND):
        """generate an error page and terminate the request

        @param request(server.Request): HTTP request
        @param core(int): error code to use
        """
        template = u'error/' + unicode(code) + '.html'

        request.setResponseCode(code)

        rendered = self.host.renderer.render(
            template,
            root_path = '/templates/',
            error_code = code,
            **request.template_data)

        self.writeData(rendered, request)
        raise failure.Failure(exceptions.CancelError(u'error page is used'))

    def writeData(self, data, request):
        """write data to transport and finish the request"""
        if data is None:
            self.pageError(request)
        request.write(data.encode('utf-8'))
        request.finish()

    def _subpagesHandler(self, dummy, request):
        """render subpage if suitable

        this method checks if there is still an unmanaged part of the path
        and check if it corresponds to a subpage. If so, it render the subpage
        else it render a NoResource.
        If there is no unmanaged part of the segment, current page workflow is pursued
        """
        if request.postpath:
            subpage = self.nextPath(request)
            try:
                child = self.children[subpage]
            except KeyError:
                self.pageError(request)
            else:
                child.render(request)
                raise failure.Failure(exceptions.CancelError(u'subpage page is used'))

    def _prepare_render(self, dummy, request):
        return defer.maybeDeferred(self.prepare_render, self, request)

    def _render_method(self, dummy, request):
        return defer.maybeDeferred(self.render_method, self, request)

    def _render_template(self, dummy, template_data):
        return self.host.renderer.render(
            self.template,
            root_path = '/templates/',
            **template_data)

    def _renderEb(self, failure_, request):
        """don't raise error on CancelError"""
        failure_.trap(exceptions.CancelError)

    def _internalError(self, failure_, request):
        """called if an error is not catched"""
        log.error(_(u"Uncatched error for HTTP request on {url}: {msg}").format(
            url = request.URLPath(),
            msg = failure_))
        self.pageError(request, C.HTTP_INTERNAL_ERROR)

    def _on_data_post_redirect(self, ret, request):
        """called when page's on_data_post has been called successfuly

        this method redirect to the same page, using Post/Redirect/Get pattern
        HTTP status code "See Other" (303) is the recommanded code in this case
        """
        session_data = self.host.getSessionData(request, session_iface.ISATSession)
        session_data.flags.add(C.FLAG_CONFIRM)
        request.setResponseCode(C.HTTP_SEE_OTHER)
        request.setHeader("location", request.uri)
        request.finish()
        raise failure.Failure(exceptions.CancelError(u'Post/Redirect/Get is used'))

    def _on_data_post(self, dummy, request):
        csrf_token = self.host.getSessionData(request, session_iface.ISATSession).csrf_token
        try:
            given_csrf = self.getPostedData(request, u'csrf_token')
        except KeyError:
            given_csrf = None
        if given_csrf is None or given_csrf != csrf_token:
            log.warning(_(u"invalid CSRF token, hack attempt? URL: {url}, IP: {ip}").format(
                url=request.uri,
                ip=request.getClientIP()))
            self.pageError(request, C.HTTP_UNAUTHORIZED)
        d = defer.maybeDeferred(self.on_data_post, self, request)
        d.addCallback(self._on_data_post_redirect, request)
        return d


    def getPostedData(self, request, keys, multiple=False):
        """get data from a POST request and decode it

        @param request(server.Request): request linked to the session
        @param keys(unicode, iterable[unicode]): name of the value(s) to get
            unicode to get one value
            iterable to get more than one
        @param multiple(bool): True if multiple values are possible/expected
            if False, the first value is returned
        @return (iterator[unicode], list[iterator[unicode], unicode, list[unicode]): values received for this(these) key(s)
        @raise KeyError: one specific key has been requested, and it is missing
        """
        if isinstance(keys, basestring):
            keys = [keys]
            get_first = True
        else:
            get_first = False

        ret = []
        for key in keys:
            gen = (urllib.unquote(v).decode('utf-8') for v in request.args.get(key,[]))
            if multiple:
                ret.append(gen)
            else:
                try:
                    ret.append(next(gen))
                except StopIteration:
                    return KeyError(key)

        return ret[0] if get_first else ret

    def getAllPostedData(self, request, except_=()):
        """get all posted data

        @param request(server.Request): request linked to the session
        @param except_(iterable[unicode]): key of values to ignore
            csrf_token will always be ignored
        @return (dict[unicode, list[unicode]]): post values
        """
        except_ = tuple(except_) + (u'csrf_token',)
        ret = {}
        for key, values in request.args.iteritems():
            key = urllib.unquote(key).decode('utf-8')
            if key in except_:
                continue
            ret[key] = [urllib.unquote(v).decode('utf-8') for v in values]
        return ret

    def getProfile(self, request):
        """helper method to easily get current profile

        @return (unicode, None): current profile
            None if no profile session is started
        """
        sat_session = self.host.getSessionData(request, session_iface.ISATSession)
        return sat_session.profile

    def getRData(self, request):
        """helper method to get request data dict

        this dictionnary if for the request only, it is not saved in session
        It is mainly used to pass data between pages/methods called during request workflow
        @return (dict): request data
        """
        try:
            return request.data
        except AttributeError:
            request.data = {}
            return request.data

    def _checkAccess(self, data, request):
        """Check access according to self.access

        if access is not granted, show a HTTP_UNAUTHORIZED pageError and stop request,
        else return data (so it can be inserted in deferred chain
        """
        if self.access == C.PAGES_ACCESS_PUBLIC:
            pass
        elif self.access == C.PAGES_ACCESS_PROFILE:
            profile = self.getProfile(request)
            if not profile:
                # no session started, access is not granted
                self.pageError(request, C.HTTP_UNAUTHORIZED)

        return data

    def renderPage(self, request, skip_parse_url=False):
        """Main method to handle the workflow of a LiberviaPage"""
        # template_data are the variables passed to template
        if not hasattr(request, 'template_data'):
            session_data = self.host.getSessionData(request, session_iface.ISATSession)
            csrf_token = session_data.csrf_token
            request.template_data = {u'csrf_token': csrf_token}
            if C.FLAG_CONFIRM in session_data.flags:
                request.template_data[u'confirm'] = True
                session_data.flags.remove(C.FLAG_CONFIRM)

            # XXX: here is the code which need to be executed once
            #      at the beginning of the request hanling
            if request.postpath and not request.postpath[-1]:
                # we don't differenciate URLs finishing with '/' or not
                del request.postpath[-1]

        d = defer.Deferred()
        d.addCallback(self._checkAccess, request)

        if self.redirect is not None:
            self.pageRedirect(self.redirect, request, skip_parse_url=False)

        if self.parse_url is not None and not skip_parse_url:
            d.addCallback(self.parse_url, request)

        d.addCallback(self._subpagesHandler, request)

        if request.method not in (C.HTTP_METHOD_GET, C.HTTP_METHOD_POST):
            # only HTTP GET and POST are handled so far
            d.addCallback(lambda dummy: self.pageError(request, C.HTTP_BAD_REQUEST))


        if request.method == C.HTTP_METHOD_POST:
            if self.on_data_post is None:
                # if we don't have on_data_post, the page was not expecting POST
                # so we return an error
                d.addCallback(lambda dummy: self.pageError(request, C.HTTP_BAD_REQUEST))
            else:
                d.addCallback(self._on_data_post, request)
            # by default, POST follow normal behaviour after on_data_post is called
            # this can be changed by a redirection or other method call in on_data_post

        if self.prepare_render:
            d.addCallback(self._prepare_render, request)

        if self.template:
            d.addCallback(self._render_template, request.template_data)
        elif self.render_method:
            d.addCallback(self._render_method, request)

        d.addCallback(self.writeData, request)
        d.addErrback(self._renderEb, request)
        d.addErrback(self._internalError, request)
        d.callback(self)
        return server.NOT_DONE_YET

    def render_GET(self, request):
        return self.renderPage(request)

    def render_POST(self, request):
        return self.renderPage(request)


class Libervia(service.Service):

    def __init__(self, options):
        self.options = options
        self.initialised = defer.Deferred()
        self.waiting_profiles = WaitingRequests()  # FIXME: should be removed

        if self.options['base_url_ext']:
            self.base_url_ext = self.options.pop('base_url_ext')
            if self.base_url_ext[-1] != '/':
                self.base_url_ext += '/'
            self.base_url_ext_data = urlparse.urlsplit(self.base_url_ext)
        else:
            self.base_url_ext = None
            # we split empty string anyway so we can do things like
            # scheme = self.base_url_ext_data.scheme or 'https'
            self.base_url_ext_data = urlparse.urlsplit('')

        if not self.options['port_https_ext']:
            self.options['port_https_ext'] = self.options['port_https']
        if self.options['data_dir'] == DATA_DIR_DEFAULT:
            coerceDataDir(self.options['data_dir'])  # this is not done when using the default value

        self.html_dir = os.path.join(self.options['data_dir'], C.HTML_DIR)
        self.themes_dir = os.path.join(self.options['data_dir'], C.THEMES_DIR)

        self._cleanup = []

        self.signal_handler = SignalHandler(self)
        self.sessions = {}  # key = session value = user
        self.prof_connected = set()  # Profiles connected

        ## bridge ##
        try:
            self.bridge = Bridge()
        except BridgeExceptionNoService:
            print(u"Can't connect to SàT backend, are you sure it's launched ?")
            sys.exit(1)
        self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb)

    def backendReady(self, dummy):
        self.root = root = LiberviaRootResource(self.options, self.html_dir)
        _register = Register(self)
        _upload_radiocol = UploadManagerRadioCol(self)
        _upload_avatar = UploadManagerAvatar(self)
        self.signal_handler.plugRegister(_register)
        self.bridge.register_signal("connected", self.signal_handler.connected)
        self.bridge.register_signal("disconnected", self.signal_handler.disconnected)
        #core
        for signal_name in ['presenceUpdate', 'messageNew', 'subscribe', 'contactDeleted',
                            'newContact', 'entityDataUpdated', 'paramUpdate']:
            self.bridge.register_signal(signal_name, self.signal_handler.getGenericCb(signal_name))
        # XXX: actionNew is handled separately because the handler must manage security_limit
        self.bridge.register_signal('actionNew', self.signal_handler.actionNewHandler)
        #plugins
        for signal_name in ['psEvent', 'mucRoomJoined', 'tarotGameStarted', 'tarotGameNew', 'tarotGameChooseContrat',
                            'tarotGameShowCards', 'tarotGameInvalidCards', 'tarotGameCardsPlayed', 'tarotGameYourTurn', 'tarotGameScore', 'tarotGamePlayers',
                            'radiocolStarted', 'radiocolPreload', 'radiocolPlay', 'radiocolNoUpload', 'radiocolUploadOk', 'radiocolSongRejected', 'radiocolPlayers',
                            'mucRoomLeft', 'mucRoomUserChangedNick', 'chatStateReceived']:
            self.bridge.register_signal(signal_name, self.signal_handler.getGenericCb(signal_name), "plugin")
        self.media_dir = self.bridge.getConfig('', 'media_dir')
        self.local_dir = self.bridge.getConfig('', 'local_dir')
        self.cache_root_dir = os.path.join(
            self.local_dir,
            C.CACHE_DIR)

        # JSON APIs
        self.putChild('json_signal_api', self.signal_handler)
        self.putChild('json_api', MethodHandler(self))
        self.putChild('register_api', _register)

        # files upload
        self.putChild('upload_radiocol', _upload_radiocol)
        self.putChild('upload_avatar', _upload_avatar)

        # static pages
        self.putChild('blog', MicroBlog(self))
        self.putChild(C.THEMES_URL, ProtectedFile(self.themes_dir))

        LiberviaPage.importPages(self)

        # media dirs
        # FIXME: get rid of dirname and "/" in C.XXX_DIR
        self.putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir))
        self.cache_resource = web_resource.NoResource()
        self.putChild(C.CACHE_DIR, self.cache_resource)

        # special
        self.putChild('radiocol', ProtectedFile(_upload_radiocol.getTmpDir(), defaultType="audio/ogg"))  # FIXME: We cheat for PoC because we know we are on the same host, so we use directly upload dir
        # pyjamas tests, redirected only for dev versions
        if self.version[-1] == 'D':
            self.putChild('test', web_util.Redirect('/libervia_test.html'))


        wrapped = web_resource.EncodingResourceWrapper(root, [server.GzipEncoderFactory()])
        self.site = server.Site(wrapped)
        self.site.sessionFactory = LiberviaSession
        self.renderer = template.Renderer(self)
        self.putChild('templates', ProtectedFile(self.renderer.base_dir))


    def _bridgeCb(self):
        self.bridge.getReady(lambda: self.initialised.callback(None),
                             lambda failure: self.initialised.errback(Exception(failure)))
        self.initialised.addCallback(self.backendReady)
        self.initialised.addErrback(lambda failure: log.error(u"Init error: %s" % failure))

    def _bridgeEb(self, failure):
        log.error(u"Can't connect to bridge: {}".format(failure))

    @property
    def version(self):
        """Return the short version of Libervia"""
        return C.APP_VERSION

    @property
    def full_version(self):
        """Return the full version of Libervia (with extra data when in development mode)"""
        version = self.version
        if version[-1] == 'D':
            # we are in debug version, we add extra data
            try:
                return self._version_cache
            except AttributeError:
                self._version_cache = u"{} ({})".format(version, utils.getRepositoryData(libervia))
                return self._version_cache
        else:
            return version

    def bridgeCall(self, method_name, *args, **kwargs):
        """Call an asynchronous bridge method and return a deferred

        @param method_name: name of the method as a unicode
        @return: a deferred which trigger the result

        """
        d = defer.Deferred()

        def _callback(*args):
            if not args:
                d.callback(None)
            else:
                if len(args) != 1:
                    Exception("Multiple return arguments not supported")
                d.callback(args[0])

        def _errback(result):
            d.errback(failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, result.classname)))

        kwargs["callback"] = _callback
        kwargs["errback"] = _errback
        getattr(self.bridge, method_name)(*args, **kwargs)
        return d

    def _logged(self, profile, request):
        """Set everything when a user just logged in

        @param profile
        @param request
        @return: a constant indicating the state:
            - C.PROFILE_LOGGED
            - C.PROFILE_LOGGED_EXT_JID
        @raise exceptions.ConflictError: session is already active
        """
        register_with_ext_jid = self.waiting_profiles.getRegisterWithExtJid(profile)
        self.waiting_profiles.purgeRequest(profile)
        _session = request.getSession()
        sat_session = session_iface.ISATSession(_session)
        if sat_session.profile:
            log.error(_(u'/!\\ Session has already a profile, this should NEVER happen!'))
            raise failure.Failure(exceptions.ConflictError("Already active"))

        sat_session.profile = profile
        self.prof_connected.add(profile)
        cache_dir = os.path.join(self.cache_root_dir, regex.pathEscape(profile))
        # FIXME: would be better to have a global /cache URL which redirect to profile's cache directory, without uuid
        self.cache_resource.putChild(sat_session.uuid, ProtectedFile(cache_dir))
        log.debug(_(u"profile cache resource added from {uuid} to {path}").format(uuid=sat_session.uuid, path=cache_dir))

        def onExpire():
            log.info(u"Session expired (profile={profile})".format(profile=profile,))
            self.cache_resource.delEntity(sat_session.uuid)
            log.debug(_(u"profile cache resource {uuid} deleted").format(uuid = sat_session.uuid))
            try:
                #We purge the queue
                del self.signal_handler.queue[profile]
            except KeyError:
                pass
            #and now we disconnect the profile
            self.bridge.disconnect(profile)

        _session.notifyOnExpire(onExpire)

        return C.PROFILE_LOGGED_EXT_JID if register_with_ext_jid else C.PROFILE_LOGGED

    @defer.inlineCallbacks
    def connect(self, request, login, password):
        """log user in

        If an other user was already logged, it will be unlogged first
        @param request(server.Request): request linked to the session
        @param login(unicode): user login
            can be profile name
            can be profile@[libervia_domain.ext]
            can be a jid (a new profile will be created with this jid if needed)
        @param password(unicode): user password
        @return (unicode, None): C.SESSION_ACTIVE: if session was aleady active else self._logged value
        @raise exceptions.DataError: invalid login
        @raise exceptions.ProfileUnknownError: this login doesn't exist
        @raise exceptions.PermissionError: a login is not accepted (e.g. empty password not allowed)
        @raise exceptions.NotReady: a profile connection is already waiting
        @raise exceptions.TimeoutError: didn't received and answer from Bridge
        @raise exceptions.InternalError: unknown error
        @raise ValueError(C.PROFILE_AUTH_ERROR): invalid login and/or password
        @raise ValueError(C.XMPP_AUTH_ERROR): invalid XMPP account password
        """

        # XXX: all security checks must be done here, even if present in javascript
        if login.startswith('@'):
            raise failure.Failure(exceptions.DataError('No profile_key allowed'))

        if '@' in login:
            try:
                login_jid = jid.JID(login)
            except (RuntimeError, jid.InvalidFormat, AttributeError):
                raise failure.Failure(exceptions.DataError('No profile_key allowed'))

            # FIXME: should it be cached?
            new_account_domain = yield self.bridgeCall("getNewAccountDomain")

            if login_jid.host == new_account_domain:
                # redirect "user@libervia.org" to the "user" profile
                login = login_jid.user
                login_jid = None
        else:
            login_jid = None

        try:
            profile = yield self.bridgeCall("profileNameGet", login)
        except Exception:  # XXX: ProfileUnknownError wouldn't work, it's encapsulated
            # FIXME: find a better way to handle bridge errors
            if login_jid is not None and login_jid.user:  # try to create a new sat profile using the XMPP credentials
                if not self.options["allow_registration"]:
                    log.warning(u"Trying to register JID account while registration is not allowed")
                    raise failure.Failure(exceptions.DataError(u"JID login while registration is not allowed"))
                profile = login # FIXME: what if there is a resource?
                connect_method = "asyncConnectWithXMPPCredentials"
                register_with_ext_jid = True
            else: # non existing username
                raise failure.Failure(exceptions.ProfileUnknownError())
        else:
            if profile != login or (not password and profile not in self.options['empty_password_allowed_warning_dangerous_list']):
                # profiles with empty passwords are restricted to local frontends
                raise exceptions.PermissionError
            register_with_ext_jid = False

            connect_method = "connect"

        # we check if there is not already an active session
        sat_session = session_iface.ISATSession(request.getSession())
        if sat_session.profile:
            # yes, there is
            if sat_session.profile != profile:
                # it's a different profile, we need to disconnect it
                log.warning(_(u"{new_profile} requested login, but {old_profile} was already connected, disconnecting {old_profile}").format(
                    old_profile = sat_session.profile,
                    new_profile = profile))
                self.purgeSession(request)

        if self.waiting_profiles.getRequest(profile):
            # FIXME: check if and when this can happen
            raise failure.Failure(exceptions.NotReady("Already waiting"))

        self.waiting_profiles.setRequest(request, profile, register_with_ext_jid)
        try:
            connected = yield self.bridgeCall(connect_method, profile, password)
        except Exception as failure_:
            fault = failure_.faultString
            self.waiting_profiles.purgeRequest(profile)
            if fault in ('PasswordError', 'ProfileUnknownError'):
                log.info(u"Profile {profile} doesn't exist or the submitted password is wrong".format(profile=profile))
                raise failure.Failure(ValueError(C.PROFILE_AUTH_ERROR))
            elif fault == 'SASLAuthError':
                log.info(u"The XMPP password of profile {profile} is wrong".format(profile=profile))
                raise failure.Failure(ValueError(C.XMPP_AUTH_ERROR))
            elif fault == 'NoReply':
                log.info(_("Did not receive a reply (the timeout expired or the connection is broken)"))
                raise exceptions.TimeOutError
            else:
                log.error(u'Unmanaged fault string "{fault}" in errback for the connection of profile {profile}'.format(
                    fault=fault, profile=profile))
                raise failure.Failure(exceptions.InternalError(fault))

        if connected:
            # profile is already connected in backend
            # do we have a corresponding session in Libervia?
            sat_session = session_iface.ISATSession(request.getSession())
            if sat_session.profile:
                # yes, session is active
                if sat_session.profile != profile:
                    # existing session should have been ended above
                    # so this line should never be reached
                    log.error(_(u'session profile [{session_profile}] differs from login profile [{profile}], this should not happen!').format(
                        session_profile = sat_session.profile,
                        profile = profile
                        ))
                    raise exceptions.InternalError("profile mismatch")
                defer.returnValue(C.SESSION_ACTIVE)
            log.info(_(u"profile {profile} was already connected in backend".format(profile=profile)))
            # no, we have to create it
        defer.returnValue(self._logged(profile, request))

    def registerNewAccount(self, request, login, password, email):
        """Create a new account, or return error
        @param request(server.Request): request linked to the session
        @param login(unicode): new account requested login
        @param email(unicode): new account email
        @param password(unicode): new account password
        @return(unicode): a constant indicating the state:
            - C.BAD_REQUEST: something is wrong in the request (bad arguments)
            - C.INVALID_INPUT: one of the data is not valid
            - C.REGISTRATION_SUCCEED: new account has been successfully registered
            - C.ALREADY_EXISTS: the given profile already exists
            - C.INTERNAL_ERROR or any unmanaged fault string
        @raise PermissionError: registration is now allowed in server configuration
        """
        if not self.options["allow_registration"]:
            log.warning(_(u"Registration received while it is not allowed, hack attempt?"))
            raise failure.Failure(exceptions.PermissionError(u"Registration is not allowed on this server"))

        if not re.match(C.REG_LOGIN_RE, login) or \
           not re.match(C.REG_EMAIL_RE, email, re.IGNORECASE) or \
           len(password) < C.PASSWORD_MIN_LENGTH:
            return C.INVALID_INPUT

        def registered(result):
            return C.REGISTRATION_SUCCEED

        def registeringError(failure):
            status = failure.value.faultString
            if status == "ConflictError":
                return C.ALREADY_EXISTS
            elif status == "InternalError":
                return C.INTERNAL_ERROR
            else:
                log.error(_(u'Unknown registering error status: {status }').format(
                    status = status))
                return status

        d = self.bridgeCall("registerSatAccount", email, password, login)
        d.addCallback(registered)
        d.addErrback(registeringError)
        return d

    def addCleanup(self, callback, *args, **kwargs):
        """Add cleaning method to call when service is stopped

        cleaning method will be called in reverse order of they insertion
        @param callback: callable to call on service stop
        @param *args: list of arguments of the callback
        @param **kwargs: list of keyword arguments of the callback"""
        self._cleanup.insert(0, (callback, args, kwargs))

    def startService(self):
        """Connect the profile for Libervia and start the HTTP(S) server(s)"""
        def eb(e):
            log.error(_(u"Connection failed: %s") % e)
            self.stop()

        def initOk(dummy):
            try:
                connected = self.bridge.isConnected(C.SERVICE_PROFILE)
            except Exception as e:
                # we don't want the traceback
                msg = [l for l in unicode(e).split('\n') if l][-1]
                log.error(u"Can't check service profile ({profile}), are you sure it exists ?\n{error}".format(
                    profile=C.SERVICE_PROFILE, error=msg))
                self.stop()
                return
            if not connected:
                self.bridge.connect(C.SERVICE_PROFILE, self.options['passphrase'],
                                    {}, callback=self._startService, errback=eb)
            else:
                self._startService()

        self.initialised.addCallback(initOk)

    ## URLs ##

    def putChild(self, path, resource):
        """Add a child to the root resource"""
        # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders)
        self.root.putChild(path, web_resource.EncodingResourceWrapper(resource, [server.GzipEncoderFactory()]))

    ## Sessions ##

    def purgeSession(self, request):
        """helper method to purge a session during request handling"""
        session = request.session
        if session is not None:
            log.debug(_(u"session purge"))
            session.expire()
            # FIXME: not clean but it seems that it's the best way to reset
            #        session during request handling
            request._secureSession = request._insecureSession = None

    def getSessionData(self, request, *args):
        """helper method to retrieve session data

        @param request(server.Request): request linked to the session
        @param *args(zope.interface.Interface): interface of the session to get
        @return (iterator(data)): requested session data
        """
        session = request.getSession()
        if len(args) == 1:
            return args[0](session)
        else:
            return (iface(session) for iface in args)

    ## TLS related methods ##

    def _TLSOptionsCheck(self):
        """Check options coherence if TLS is activated, and update missing values

        Must be called only if TLS is activated
        """
        if not self.options['tls_certificate']:
            log.error(u"a TLS certificate is needed to activate HTTPS connection")
            self.quit(1)
        if not self.options['tls_private_key']:
            self.options['tls_private_key'] = self.options['tls_certificate']


        if not self.options['tls_private_key']:
            self.options['tls_private_key'] = self.options['tls_certificate']

    def _loadCertificates(self, f):
        """Read a .pem file with a list of certificates

        @param f (file): file obj (opened .pem file)
        @return (list[OpenSSL.crypto.X509]): list of certificates
        @raise OpenSSL.crypto.Error: error while parsing the file
        """
        # XXX: didn't found any method to load a .pem file with several certificates
        #      so the certificates split is done here
        certificates = []
        buf = []
        while True:
            line = f.readline()
            buf.append(line)
            if '-----END CERTIFICATE-----' in line:
                certificates.append(OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, ''.join(buf)))
                buf=[]
            elif not line:
                log.debug(u"{} certificate(s) found".format(len(certificates)))
                return certificates

    def _loadPKey(self, f):
        """Read a private key from a .pem file

        @param f (file): file obj (opened .pem file)
        @return (list[OpenSSL.crypto.PKey]): private key object
        @raise OpenSSL.crypto.Error: error while parsing the file
        """
        return OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, f.read())

    def _loadCertificate(self, f):
        """Read a public certificate from a .pem file

        @param f (file): file obj (opened .pem file)
        @return (list[OpenSSL.crypto.X509]): public certificate
        @raise OpenSSL.crypto.Error: error while parsing the file
        """
        return OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read())

    def _getTLSContextFactory(self):
        """Load TLS certificate and build the context factory needed for listenSSL"""
        if ssl is None:
            raise ImportError(u"Python module pyOpenSSL is not installed!")

        cert_options = {}

        for name, option, method in [('privateKey', 'tls_private_key', self._loadPKey),
                                    ('certificate', 'tls_certificate', self._loadCertificate),
                                    ('extraCertChain', 'tls_chain', self._loadCertificates)]:
            path = self.options[option]
            if not path:
                assert option=='tls_chain'
                continue
            log.debug(u"loading {option} from {path}".format(option=option, path=path))
            try:
                with open(path) as f:
                    cert_options[name] = method(f)
            except IOError as e:
                log.error(u"Error while reading file {path} for option {option}: {error}".format(path=path, option=option, error=e))
                self.quit(2)
            except OpenSSL.crypto.Error:
                log.error(u"Error while parsing file {path} for option {option}, are you sure it is a valid .pem file?".format(path=path, option=option))
                if option=='tls_private_key' and self.options['tls_certificate'] == path:
                    log.error(u"You are using the same file for private key and public certificate, make sure that both a in {path} or use --tls_private_key option".format(path=path))
                self.quit(2)

        return ssl.CertificateOptions(**cert_options)

    ## service management ##

    def _startService(self, dummy=None):
        """Actually start the HTTP(S) server(s) after the profile for Libervia is connected.

        @raise ImportError: OpenSSL is not available
        @raise IOError: the certificate file doesn't exist
        @raise OpenSSL.crypto.Error: the certificate file is invalid
        """
        # now that we have service profile connected, we add resource for its cache
        service_path = regex.pathEscape(C.SERVICE_PROFILE)
        cache_dir = os.path.join(self.cache_root_dir, service_path)
        self.cache_resource.putChild(service_path, ProtectedFile(cache_dir))
        self.service_cache_url = os.path.join(C.CACHE_DIR, service_path)

        if self.options['connection_type'] in ('https', 'both'):
            self._TLSOptionsCheck()
            context_factory = self._getTLSContextFactory()
            reactor.listenSSL(self.options['port_https'], self.site, context_factory)
        if self.options['connection_type'] in ('http', 'both'):
            if self.options['connection_type'] == 'both' and self.options['redirect_to_https']:
                reactor.listenTCP(self.options['port'], server.Site(RedirectToHTTPS(self.options['port'], self.options['port_https_ext'])))
            else:
                reactor.listenTCP(self.options['port'], self.site)

    def stopService(self):
        log.info(_("launching cleaning methods"))
        for callback, args, kwargs in self._cleanup:
            callback(*args, **kwargs)
        try:
            self.bridge.disconnect(C.SERVICE_PROFILE)
        except Exception:
            log.warning(u"Can't disconnect service profile")

    def run(self):
        reactor.run()

    def stop(self):
        reactor.stop()

    def quit(self, exit_code=None):
        """Exit app when reactor is running

        @param exit_code(None, int): exit code
        """
        self.stop()
        sys.exit(exit_code or 0)


class RedirectToHTTPS(web_resource.Resource):

    def __init__(self, old_port, new_port):
        web_resource.Resource.__init__(self)
        self.isLeaf = True
        self.old_port = old_port
        self.new_port = new_port

    def render(self, request):
        netloc = request.URLPath().netloc.replace(':%s' % self.old_port, ':%s' % self.new_port)
        url = "https://" + netloc + request.uri
        return web_util.redirectTo(url, request)


registerAdapter(session_iface.SATSession, server.Session, session_iface.ISATSession)
registerAdapter(session_iface.SATGuestSession, server.Session, session_iface.ISATGuestSession)