view src/server/pages.py @ 994:b92b06f023cb

pages: profile is now always set in template_data, and None if there is no user logged
author Goffi <goffi@goffi.org>
date Wed, 13 Dec 2017 00:37:12 +0100
parents 6daa59d44ee2
children f88325b56a6a
line wrap: on
line source

#!/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.words.protocols.jabber import jid
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
import time


class Cache(object):

    def __init__(self, rendered):
        self._created = time.time()
        self._last_access = self._created
        self._rendered = rendered

    @property
    def created(self):
        return self._created

    @property
    def last_access(self):
        return self._last_access

    @last_access.setter
    def last_access(self, timestamp):
        self._last_access = timestamp

    @property
    def rendered(self):
        return self._rendered


class LiberviaPage(web_resource.Resource):
    isLeaf = True  # we handle subpages ourself
    named_pages = {}
    uri_callbacks = {}
    pages_redirects = {}
    cache = {}
    # Set of tuples (service/node/sub_id) of nodes subscribed for caching
    # sub_id can be empty string if not handled by service
    cache_pubsub_sub = set()
    main_menu = None

    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"))

        # if not None, next rendering will be cached
        # it must then contain a list of the the keys to use (without the page instance)
        # e.g. [C.SERVICE_PROFILE, "pubsub", server@example.tld, pubsub_node] 
        self._do_cache = None

    def __unicode__(self):
        return u'LiberviaPage {name} at {url}'.format(
            name = self.name or u'<anonymous>',
            url = self.url)

    def __str__(self):
        return self.__unicode__.encode('utf-8')

    @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 setMenu(cls, menus):
        main_menu = []
        for menu in menus:
            if not menu:
                raise ValueError(_(u"menu item can't be empty"))
            elif isinstance(menu, list):
                if len(menu) != 2:
                    raise ValueError(_(u"menu item as list must be in the form [page_name, absolue URL]"))
                page_name, url = menu
            else:
                page_name = menu
                url = cls.getPageByName(page_name).url
            main_menu.append((page_name, url))
        cls.main_menu = main_menu

    @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 checkCacheSubscribeCb(self, sub_id, service, node):
        self.cache_pubsub_sub.add((service, node, sub_id))

    def checkCacheSubscribeEb(self, failure_, service, node):
        log.warning(_(u"Can't subscribe to node: {msg}").format(msg=failure_))
        # FIXME: cache must be marked as unusable here

    def psNodeWatchAddEb(self, failure_, service, node):
        log.warning(_(u"Can't add node watched: {msg}").format(msg=failure_))

    def checkCache(self, request, cache_type, **kwargs):
        """check if a page is in cache and return cached version if suitable

        this method may perform extra operation to handle cache (e.g. subscribing to a
        pubsub node)
        @param request(server.Request): current HTTP request
        @param cache_type(int): on of C.CACHE_* const.
        @param **kwargs: args according to cache_type:
            C.CACHE_PROFILE:
                service: pubsub service
                node: pubsub node
                short: short name of feature (needed if node is empty)

        """
        if request.postpath:
            # we are not on the final page, no need to go further
            return
        profile = self.getProfile(request) or C.SERVICE_PROFILE

        if cache_type == C.CACHE_PUBSUB:
            service, node = kwargs['service'], kwargs['node']
            if not node:
                try:
                    short = kwargs['short']
                    node = self.host.ns_map[short]
                except KeyError:
                    log.warning(_(u"Can't use cache for empty node without namespace set, please ensure to set \"short\" and that it is registered"))
                    return
            if profile != C.SERVICE_PROFILE:
                # only service profile is cache for now
                return
            try:
                cache = self.cache[profile][cache_type][service][node][self]
            except KeyError:
                # no cache yet, let's subscribe to the pubsub node
                d1 = self.host.bridgeCall('psSubscribe', service.full(), node, {}, profile)
                d1.addCallback(self.checkCacheSubscribeCb, service, node)
                d1.addErrback(self.checkCacheSubscribeEb, service, node)
                d2 = self.host.bridgeCall('psNodeWatchAdd', service.full(), node, profile)
                d2.addErrback(self.psNodeWatchAddEb, service, node)
                self._do_cache = [profile, cache_type, service, node]
                # we don't return the Deferreds as it is not needed to wait for
                # the subscription to continue with page rendering
                return

        else:
            raise exceptions.InternalError(u'Unknown cache_type')
        log.debug(u'using cache for {page}'.format(page=self))
        cache.last_access = time.time()
        request.write(cache.rendered)
        request.finish()
        raise failure.Failure(exceptions.CancelError(u'cache is used'))

    @classmethod
    def onNodeEvent(cls, host, service, node, event_type, items, profile):
        """Invalidate cache for all pages linked to this node"""
        try:
            cache = cls.cache[profile][C.CACHE_PUBSUB][jid.JID(service)][node]
        except KeyError:
            log.info(_(u'Removing subscription for {service}/{node}: '
                       u'the page is not cached').format(service=service, node=node))
            d1 = host.bridgeCall('psUnsubscribe', service, node, profile)
            d1.addErrback(lambda failure_:
                log.warning(_(u"Can't unsubscribe from {service}/{node}: {msg}").format(
                    service=service, node=node, msg=failure_)))
            d2 = host.bridgeCall('psNodeWatchAdd', service, node, profile)
            # TODO: check why the page is not in cache, remove subscription?
            d2.addErrback(lambda failure_:
                log.warning(_(u"Can't remove watch for {service}/{node}: {msg}").format(
                    service=service, node=node, msg=failure_)))
        else:
            cache.clear()

    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)
        data_encoded = data.encode('utf-8')
        request.write(data_encoded)
        request.finish()
        if self._do_cache is not None:
            cache = reduce(lambda d, k: d.setdefault(k, {}), self._do_cache, self.cache)
            cache[self] = Cache(data_encoded)
            log.debug(_(u'{page} put in cache for [{profile}]').format(
                page=self,
                profile=self._do_cache[0]))
            self._do_cache = None

    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,
            main_menu = LiberviaPage.main_menu,
            **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'profile': session_data.profile,
                                     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)