changeset 984:f0fc28b3bd1e

server: moved LiberviaPage code in its own module
author Goffi <goffi@goffi.org>
date Fri, 17 Nov 2017 12:10:56 +0100
parents 8c9fdb58de5f
children 64826e69f365
files src/server/pages.py src/server/server.py src/server/utils.py
diffstat 3 files changed, 699 insertions(+), 643 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/server/pages.py	Fri Nov 17 12:10:56 2017 +0100
@@ -0,0 +1,673 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2017 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.web import server
+from twisted.web import resource as web_resource
+from twisted.web import util as web_util
+from twisted.internet import defer
+from twisted.python import failure
+
+from sat.core.i18n import _
+from sat.core import exceptions
+from sat.tools.common import uri as common_uri
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from libervia.server.constants import Const as C
+from libervia.server import session_iface
+from libervia.server.utils import quote
+import libervia
+
+import os.path
+import urllib
+
+
+class LiberviaPage(web_resource.Resource):
+    isLeaf = True  # we handle subpages ourself
+    named_pages = {}
+    uri_callbacks = {}
+    pages_redirects = {}
+
+    def __init__(self, host, root_dir, url, 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 file path of the page
+        @param url(unicode): relative URL to the page
+            this URL may not be valid, as pages may require path arguments
+        @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 redirected. 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
+            on_data_post can return a string with following value:
+                - C.POST_NO_CONFIRM: confirm flag will not be set
+        """
+
+        web_resource.Resource.__init__(self)
+        self.host = host
+        self.root_dir = root_dir
+        self.url = url
+        self.name = name
+        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
+            # so we check that rendering methods/values are not set
+            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 = {}
+                new_path = path + [d]
+                # 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,
+                    u'/' + u'/'.join(new_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)
+                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
+
+    @classmethod
+    def getPageByName(cls, 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 cls.named_pages[name]
+
+    def getPageRedirectURL(self, request, page_name=u'login', url=None):
+        """generate URL for a page with redirect_url parameter set
+
+        mainly used for login page with redirection to current page
+        @param request(server.Request): current HTTP request
+        @param page_name(unicode): name of the page to go
+        @param url(None, unicode): url to redirect to
+            None to use request path (i.e. current page)
+        @return (unicode): URL to use
+        """
+        return u'{root_url}?redirect_url={redirect_url}'.format(
+            root_url = self.getPageByName(page_name).url,
+            redirect_url=urllib.quote_plus(request.uri) if url is None else url.encode('utf-8'))
+
+    def getURL(self, *args):
+        """retrieve URL of the page set arguments
+
+        *args(list[unicode]): argument to add to the URL as path elements
+        """
+        url_args = [quote(a) for a in args]
+
+        if self.name is not None and self.name in self.pages_redirects:
+            # we check for redirection
+            redirect_data = self.pages_redirects[self.name]
+            args_hash = tuple(args)
+            for limit in xrange(len(args)+1):
+                current_hash = args_hash[:limit]
+                if current_hash in redirect_data:
+                    url_base = redirect_data[current_hash]
+                    remaining = args[limit:]
+                    remaining_url = '/'.join(remaining)
+                    return os.path.join('/', url_base, remaining_url)
+
+        return os.path.join(self.url, *url_args)
+
+    def getSubPageURL(self, request, page_name, *args):
+        """retrieve a page in direct children and build its URL according to request
+
+        request's current path is used as base (at current parsing point,
+        i.e. it's more prepath than path).
+        Requested page is checked in children and an absolute URL is then built
+        by the resulting combination.
+        This method is useful to construct absolute URLs for children instead of
+        using relative path, which may not work in subpages, and are linked to the
+        names of directories (i.e. relative URL will break if subdirectory is renamed
+        while getSubPageURL won't as long as page_name is consistent).
+        Also, request.path is used, keeping real path used by user,
+        and potential redirections.
+        @param request(server.Request): current HTTP request
+        @param page_name(unicode): name of the page to retrieve
+            it must be a direct children of current page
+        @param *args(list[unicode]): arguments to add as path elements
+        @return unicode: absolute URL to the sub page
+        """
+        # we get url in the following way (splitting request.path instead of using
+        # request.prepath) because request.prepath may have been modified by
+        # redirection (if redirection args have been specified), while path reflect
+        # the real request
+
+        # we ignore empty path elements (i.e. double '/' or '/' at the end)
+        path_elts = [p for p in request.path.split('/') if p]
+
+        if request.postpath:
+            if not request.postpath[-1]:
+                # we remove trailing slash
+                request.postpath = request.postpath[:-1]
+            if request.postpath:
+                # getSubPageURL must return subpage from the point where
+                # the it is called, so we have to remove remanining
+                # path elements
+                path_elts = path_elts[:-len(request.postpath)]
+
+        current_url = '/' + '/'.join(path_elts).decode('utf-8')
+
+        for path, child in self.children.iteritems():
+            try:
+                child_name = child.name
+            except AttributeError:
+                # LiberviaPage have a name, but maybe this is an other Resource
+                continue
+            if child_name == page_name:
+                return os.path.join(u'/', current_url, path, *args)
+        raise exceptions.NotFound(_(u'requested sub page has not been found'))
+
+    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 HTTPRedirect(self, request, url):
+        """redirect to an URL using HTTP redirection
+
+        @param request(server.Request): current HTTP request
+        @param url(unicode): url to redirect to
+        """
+
+        web_util.redirectTo(url.encode('utf-8'), request)
+        request.finish()
+        raise failure.Failure(exceptions.CancelError(u'HTTP redirection is used'))
+
+    def redirectOrContinue(self, request, redirect_arg=u'redirect_url'):
+        """helper method to redirect a page to an url given as arg
+
+        if the arg is not present, the page will continue normal workflow
+        @param request(server.Request): current HTTP request
+        @param redirect_arg(unicode): argument to use to get redirection URL
+        @interrupt: redirect the page to requested URL
+        @interrupt pageError(C.HTTP_BAD_REQUEST): empty or non local URL is used
+        """
+        try:
+            url = self.getPostedData(request, 'redirect_url')
+        except KeyError:
+            pass
+        else:
+            # a redirection is requested
+            if not url or url[0] != u'/':
+                # we only want local urls
+                self.pageError(request, C.HTTP_BAD_REQUEST)
+            else:
+                self.HTTPRedirect(request, url)
+
+    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.
+        If you want to do a HTTP redirection, use HTTPRedirect
+        @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:]:
+            if redirect_page is self.host.root:
+                redirect_page = redirect_page.children[subpage]
+            else:
+                redirect_page = redirect_page.original.children[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, request):
+        template_data = request.template_data
+
+        # if confirm variable is set in case of successfuly data post
+        session_data = self.host.getSessionData(request, session_iface.ISATSession)
+        if session_data.popPageFlag(self, C.FLAG_CONFIRM):
+            template_data[u'confirm'] = True
+
+        return self.host.renderer.render(
+            self.template,
+            root_path = '/templates/',
+            media_path = '/' + C.MEDIA_DIR,
+            **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 done successfuly
+
+        This will do a Post/Redirect/Get pattern.
+        this method redirect to the same page or to request.data['post_redirect_page']
+        post_redirect_page can be either a page or a tuple with page as first item, then a list of unicode arguments to append to the url.
+        if post_redirect_page is not used, initial request.uri (i.e. the same page as where the data have been posted) will be used for redirection.
+        HTTP status code "See Other" (303) is used as it is the recommanded code in this case.
+        @param ret(None, unicode, iterable): on_data_post return value
+            see LiberviaPage.__init__ on_data_post docstring
+        """
+        if ret is None:
+            ret = ()
+        elif isinstance(ret, basestring):
+            ret = (ret,)
+        else:
+            ret = tuple(ret)
+            raise NotImplementedError(_(u'iterable in on_data_post return value is not used yet'))
+        session_data = self.host.getSessionData(request, session_iface.ISATSession)
+        request_data = self.getRData(request)
+        if 'post_redirect_page' in request_data:
+            redirect_page_data = request_data['post_redirect_page']
+            if isinstance(redirect_page_data, tuple):
+                redirect_page = redirect_page_data[0]
+                redirect_page_args = redirect_page_data[1:]
+                redirect_uri = redirect_page.getURL(*redirect_page_args)
+            else:
+                redirect_page = redirect_page_data
+                redirect_uri = redirect_page.url
+        else:
+            redirect_page = self
+            redirect_uri = request.uri
+
+        if not C.POST_NO_CONFIRM in ret:
+            session_data.setPageFlag(redirect_page, C.FLAG_CONFIRM)
+        request.setResponseCode(C.HTTP_SEE_OTHER)
+        request.setHeader("location", redirect_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:
+                    raise 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
+                if not self.host.options["allow_registration"]:
+                    # registration not allowed, access is not granted
+                    self.pageError(request, C.HTTP_UNAUTHORIZED)
+                else:
+                    # registration allowed, we redirect to login page
+                    login_url = self.getPageRedirectURL(request)
+                    self.HTTPRedirect(request, login_url)
+
+        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}
+
+            # 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)
+        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)
--- a/src/server/server.py	Fri Nov 17 11:01:34 2017 +0100
+++ b/src/server/server.py	Fri Nov 17 12:10:56 2017 +0100
@@ -39,7 +39,6 @@
 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
@@ -52,6 +51,8 @@
 import urllib
 from httplib import HTTPS_PORT
 import libervia
+from libervia.server.pages import LiberviaPage
+from libervia.server.utils import quote
 
 try:
     import OpenSSL
@@ -68,11 +69,6 @@
 DATA_DIR_DEFAULT = OPT_PARAMETERS_BOTH = OPT_PARAMETERS_CFG = coerceDataDir = None
 
 
-def quote(value):
-    """shortcut to quote an unicode value for URL"""
-    return urllib.quote_plus(value.encode('utf-8')).replace('%40','@')
-
-
 class LiberviaSession(server.Session):
     sessionTimeout = C.SESSION_TIMEOUT
 
@@ -1366,643 +1362,6 @@
         return ("setAvatar", filepath, profile)
 
 
-class LiberviaPage(web_resource.Resource):
-    isLeaf = True  # we handle subpages ourself
-    named_pages = {}
-    uri_callbacks = {}
-    pages_redirects = {}
-
-    def __init__(self, host, root_dir, url, 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 file path of the page
-        @param url(unicode): relative URL to the page
-            this URL may not be valid, as pages may require path arguments
-        @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 redirected. 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
-            on_data_post can return a string with following value:
-                - C.POST_NO_CONFIRM: confirm flag will not be set
-        """
-
-        web_resource.Resource.__init__(self)
-        self.host = host
-        self.root_dir = root_dir
-        self.url = url
-        self.name = name
-        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
-            # so we check that rendering methods/values are not set
-            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 = {}
-                new_path = path + [d]
-                # 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,
-                    u'/' + u'/'.join(new_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)
-                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
-
-    @classmethod
-    def getPageByName(cls, 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 cls.named_pages[name]
-
-    def getPageRedirectURL(self, request, page_name=u'login', url=None):
-        """generate URL for a page with redirect_url parameter set
-
-        mainly used for login page with redirection to current page
-        @param request(server.Request): current HTTP request
-        @param page_name(unicode): name of the page to go
-        @param url(None, unicode): url to redirect to
-            None to use request path (i.e. current page)
-        @return (unicode): URL to use
-        """
-        return u'{root_url}?redirect_url={redirect_url}'.format(
-            root_url = self.getPageByName(page_name).url,
-            redirect_url=urllib.quote_plus(request.uri) if url is None else url.encode('utf-8'))
-
-    def getURL(self, *args):
-        """retrieve URL of the page set arguments
-
-        *args(list[unicode]): argument to add to the URL as path elements
-        """
-        url_args = [quote(a) for a in args]
-
-        if self.name is not None and self.name in self.pages_redirects:
-            # we check for redirection
-            redirect_data = self.pages_redirects[self.name]
-            args_hash = tuple(args)
-            for limit in xrange(len(args)+1):
-                current_hash = args_hash[:limit]
-                if current_hash in redirect_data:
-                    url_base = redirect_data[current_hash]
-                    remaining = args[limit:]
-                    remaining_url = '/'.join(remaining)
-                    return os.path.join('/', url_base, remaining_url)
-
-        return os.path.join(self.url, *url_args)
-
-    def getSubPageURL(self, request, page_name, *args):
-        """retrieve a page in direct children and build its URL according to request
-
-        request's current path is used as base (at current parsing point,
-        i.e. it's more prepath than path).
-        Requested page is checked in children and an absolute URL is then built
-        by the resulting combination.
-        This method is useful to construct absolute URLs for children instead of
-        using relative path, which may not work in subpages, and are linked to the
-        names of directories (i.e. relative URL will break if subdirectory is renamed
-        while getSubPageURL won't as long as page_name is consistent).
-        Also, request.path is used, keeping real path used by user,
-        and potential redirections.
-        @param request(server.Request): current HTTP request
-        @param page_name(unicode): name of the page to retrieve
-            it must be a direct children of current page
-        @param *args(list[unicode]): arguments to add as path elements
-        @return unicode: absolute URL to the sub page
-        """
-        # we get url in the following way (splitting request.path instead of using
-        # request.prepath) because request.prepath may have been modified by
-        # redirection (if redirection args have been specified), while path reflect
-        # the real request
-
-        # we ignore empty path elements (i.e. double '/' or '/' at the end)
-        path_elts = [p for p in request.path.split('/') if p]
-
-        if request.postpath:
-            if not request.postpath[-1]:
-                # we remove trailing slash
-                request.postpath = request.postpath[:-1]
-            if request.postpath:
-                # getSubPageURL must return subpage from the point where
-                # the it is called, so we have to remove remanining
-                # path elements
-                path_elts = path_elts[:-len(request.postpath)]
-
-        current_url = '/' + '/'.join(path_elts).decode('utf-8')
-
-        for path, child in self.children.iteritems():
-            try:
-                child_name = child.name
-            except AttributeError:
-                # LiberviaPage have a name, but maybe this is an other Resource
-                continue
-            if child_name == page_name:
-                return os.path.join(u'/', current_url, path, *args)
-        raise exceptions.NotFound(_(u'requested sub page has not been found'))
-
-    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 HTTPRedirect(self, request, url):
-        """redirect to an URL using HTTP redirection
-
-        @param request(server.Request): current HTTP request
-        @param url(unicode): url to redirect to
-        """
-
-        web_util.redirectTo(url.encode('utf-8'), request)
-        request.finish()
-        raise failure.Failure(exceptions.CancelError(u'HTTP redirection is used'))
-
-    def redirectOrContinue(self, request, redirect_arg=u'redirect_url'):
-        """helper method to redirect a page to an url given as arg
-
-        if the arg is not present, the page will continue normal workflow
-        @param request(server.Request): current HTTP request
-        @param redirect_arg(unicode): argument to use to get redirection URL
-        @interrupt: redirect the page to requested URL
-        @interrupt pageError(C.HTTP_BAD_REQUEST): empty or non local URL is used
-        """
-        try:
-            url = self.getPostedData(request, 'redirect_url')
-        except KeyError:
-            pass
-        else:
-            # a redirection is requested
-            if not url or url[0] != u'/':
-                # we only want local urls
-                self.pageError(request, C.HTTP_BAD_REQUEST)
-            else:
-                self.HTTPRedirect(request, url)
-
-    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.
-        If you want to do a HTTP redirection, use HTTPRedirect
-        @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:]:
-            if redirect_page is self.host.root:
-                redirect_page = redirect_page.children[subpage]
-            else:
-                redirect_page = redirect_page.original.children[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, request):
-        template_data = request.template_data
-
-        # if confirm variable is set in case of successfuly data post
-        session_data = self.host.getSessionData(request, session_iface.ISATSession)
-        if session_data.popPageFlag(self, C.FLAG_CONFIRM):
-            template_data[u'confirm'] = True
-
-        return self.host.renderer.render(
-            self.template,
-            root_path = '/templates/',
-            media_path = '/' + C.MEDIA_DIR,
-            **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 done successfuly
-
-        This will do a Post/Redirect/Get pattern.
-        this method redirect to the same page or to request.data['post_redirect_page']
-        post_redirect_page can be either a page or a tuple with page as first item, then a list of unicode arguments to append to the url.
-        if post_redirect_page is not used, initial request.uri (i.e. the same page as where the data have been posted) will be used for redirection.
-        HTTP status code "See Other" (303) is used as it is the recommanded code in this case.
-        @param ret(None, unicode, iterable): on_data_post return value
-            see LiberviaPage.__init__ on_data_post docstring
-        """
-        if ret is None:
-            ret = ()
-        elif isinstance(ret, basestring):
-            ret = (ret,)
-        else:
-            ret = tuple(ret)
-            raise NotImplementedError(_(u'iterable in on_data_post return value is not used yet'))
-        session_data = self.host.getSessionData(request, session_iface.ISATSession)
-        request_data = self.getRData(request)
-        if 'post_redirect_page' in request_data:
-            redirect_page_data = request_data['post_redirect_page']
-            if isinstance(redirect_page_data, tuple):
-                redirect_page = redirect_page_data[0]
-                redirect_page_args = redirect_page_data[1:]
-                redirect_uri = redirect_page.getURL(*redirect_page_args)
-            else:
-                redirect_page = redirect_page_data
-                redirect_uri = redirect_page.url
-        else:
-            redirect_page = self
-            redirect_uri = request.uri
-
-        if not C.POST_NO_CONFIRM in ret:
-            session_data.setPageFlag(redirect_page, C.FLAG_CONFIRM)
-        request.setResponseCode(C.HTTP_SEE_OTHER)
-        request.setHeader("location", redirect_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:
-                    raise 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
-                if not self.host.options["allow_registration"]:
-                    # registration not allowed, access is not granted
-                    self.pageError(request, C.HTTP_UNAUTHORIZED)
-                else:
-                    # registration allowed, we redirect to login page
-                    login_url = self.getPageRedirectURL(request)
-                    self.HTTPRedirect(request, login_url)
-
-        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}
-
-            # 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)
-        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):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/server/utils.py	Fri Nov 17 12:10:56 2017 +0100
@@ -0,0 +1,24 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2017 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/>.
+import urllib
+
+
+def quote(value):
+    """shortcut to quote an unicode value for URL"""
+    return urllib.quote_plus(value.encode('utf-8')).replace('%40','@')