diff libervia/server/pages.py @ 1124:28e3eb3bb217

files reorganisation and installation rework: - files have been reorganised to follow other SàT projects and usual Python organisation (no more "/src" directory) - VERSION file is now used, as for other SàT projects - replace the overcomplicated setup.py be a more sane one. Pyjamas part is not compiled anymore by setup.py, it must be done separatly - removed check for data_dir if it's empty - installation tested working in virtual env - libervia launching script is now in bin/libervia
author Goffi <goffi@goffi.org>
date Sat, 25 Aug 2018 17:59:48 +0200
parents src/server/pages.py@cdd389ef97bc
children 9234f29053b0
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/server/pages.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,1383 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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.tools.common import date_utils
+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, SubPage
+import libervia
+
+from collections import namedtuple
+import uuid
+import os.path
+import urllib
+import time
+import hashlib
+
+WebsocketMeta = namedtuple("WebsocketMeta", ("url", "token", "debug"))
+
+
+class CacheBase(object):
+    def __init__(self):
+        self._created = time.time()
+        self._last_access = self._created
+
+    @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
+
+
+class CachePage(CacheBase):
+    def __init__(self, rendered):
+        super(CachePage, self).__init__()
+        self._created = time.time()
+        self._last_access = self._created
+        self._rendered = rendered
+        self._hash = hashlib.sha256(rendered).hexdigest()
+
+    @property
+    def rendered(self):
+        return self._rendered
+
+    @property
+    def hash(self):
+        return self._hash
+
+
+class CacheURL(CacheBase):
+    def __init__(self, request):
+        super(CacheURL, self).__init__()
+        try:
+            self._data = request.data.copy()
+        except AttributeError:
+            self._data = {}
+        self._template_data = request.template_data.copy()
+        self._prepath = request.prepath[:]
+        self._postpath = request.postpath[:]
+        del self._template_data["csrf_token"]
+
+    def use(self, request):
+        self.last_access = time.time()
+        request.data = self._data.copy()
+        request.template_data.update(self._template_data)
+        request.prepath = self._prepath[:]
+        request.postpath = self._postpath[:]
+
+
+class LiberviaPage(web_resource.Resource):
+    isLeaf = True  #  we handle subpages ourself
+    named_pages = {}
+    uri_callbacks = {}
+    signals_handlers = {}
+    pages_redirects = {}
+    cache = {}
+    cached_urls = {}
+    #  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,
+        dynamic=False,
+        parse_url=None,
+        prepare_render=None,
+        render=None,
+        template=None,
+        on_data_post=None,
+        on_data=None,
+        on_signal=None,
+        url_cache=False,
+    ):
+        """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 dynamic(bool): if True, activate websocket for bidirectional communication
+        @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
+        @param on_data(callable, None): method to call when dynamic data is sent
+            this method is used with Libervia's websocket mechanism
+        @param on_signal(callable, None): method to call when a registered signal is received
+            this method is used with Libervia's websocket mechanism
+        """
+
+        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
+        self.dynamic = dynamic
+        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
+        self.on_data = on_data
+        self.on_signal = on_signal
+        self.url_cache = url_cache
+        if access == C.PAGES_ACCESS_NONE:
+            # none pages just return a 404, no further check is needed
+            return
+        if template is not None and render is not None:
+            log.error(_(u"render and template methods can't be used at the same time"))
+        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"),
+                    dynamic=page_data.get("dynamic", False),
+                    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"),
+                    on_data=page_data.get("on_data"),
+                    on_signal=page_data.get("on_signal"),
+                    url_cache=page_data.get("url_cache", False),
+                )
+                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:
+                                resource.registerURI(uri_tuple, cb)
+
+                LiberviaPage.importPages(host, resource, new_path)
+
+    @classmethod
+    def setMenu(cls, menus):
+        main_menu = []
+        for menu in menus:
+            if not menu:
+                msg = _(u"menu item can't be empty")
+                log.error(msg)
+                raise ValueError(msg)
+            elif isinstance(menu, list):
+                if len(menu) != 2:
+                    msg = _(
+                        u"menu item as list must be in the form [page_name, absolue URL]"
+                    )
+                    log.error(msg)
+                    raise ValueError(msg)
+                page_name, url = menu
+            else:
+                page_name = menu
+                try:
+                    url = cls.getPageByName(page_name).url
+                except KeyError as e:
+                    log.error(
+                        _(
+                            u"Can'find a named page ({msg}), please check menu_json in configuration."
+                        ).format(msg=e)
+                    )
+                    raise e
+            main_menu.append((page_name, url))
+        cls.main_menu = main_menu
+
+    def registerURI(self, uri_tuple, get_uri_cb):
+        """register a URI handler
+
+        @param uri_tuple(tuple[unicode, unicode]): type or URIs handler
+            type/subtype as returned by tools/common/parseXMPPUri
+            or type/None to handle all subtypes
+        @param get_uri_cb(callable): method which take uri_data dict as only argument
+            and return absolute path with correct arguments or None if the page
+            can't handle this URL
+        """
+        if uri_tuple in self.uri_callbacks:
+            log.info(
+                _(u"{}/{} URIs are already handled, replacing by the new handler").format(
+                    *uri_tuple
+                )
+            )
+        self.uri_callbacks[uri_tuple] = (self, get_uri_cb)
+
+    def registerSignal(self, request, signal, check_profile=True):
+        r"""register a signal handler
+
+        the page must be dynamic
+        when signal is received, self.on_signal will be called with:
+            - request
+            - signal name
+            - signal arguments
+        signal handler will be removed when connection with dynamic page will be lost
+        @param signal(unicode): name of the signal
+            last arg of signal must be profile, as it will be checked to filter signals
+        @param check_profile(bool): if True, signal profile (which MUST be last arg) will be
+            checked against session profile.
+            /!\ if False, profile will not be checked/filtered, be sure to know what you are doing
+                if you unset this option /!\
+        """
+        # FIXME: add a timeout, if socket is not opened before it, signal handler must be removed
+        if not self.dynamic:
+            log.error(_(u"You can't register signal if page is not dynamic"))
+            return
+        LiberviaPage.signals_handlers.setdefault(signal, {})[id(request)] = (
+            self,
+            request,
+            check_profile,
+        )
+        request._signals_registered.append(signal)
+
+    @classmethod
+    def getPagePathFromURI(cls, 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 no page has been registered for this URI
+        """
+        uri_data = common_uri.parseXMPPUri(uri)
+        try:
+            page, cb = cls.uri_callbacks[uri_data["type"], uri_data["sub_type"]]
+        except KeyError:
+            url = None
+        else:
+            url = cb(page, uri_data)
+        if url is None:
+            # no handler found
+            # we try to find a more generic one
+            try:
+                page, cb = cls.uri_callbacks[uri_data["type"], None]
+            except KeyError:
+                pass
+            else:
+                url = cb(page, 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
+            empty or None arguments will be ignored
+        """
+        url_args = [quote(a) for a in args if a]
+
+        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 getCurrentURL(self, request):
+        """retrieve URL used to access this page
+
+        @return(unicode): current URL
+        """
+        # 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)]
+
+        return u"/" + "/".join(path_elts).decode("utf-8")
+
+    def getParamURL(self, request, **kwargs):
+        """use URL of current request but modify the parameters in query part
+
+        **kwargs(dict[str, unicode]): argument to use as query parameters
+        @return (unicode): constructed URL
+        """
+        current_url = self.getCurrentURL(request)
+        if kwargs:
+            encoded = urllib.urlencode(
+                {k: v.encode("utf-8") for k, v in kwargs.iteritems()}
+            ).decode("utf-8")
+            current_url = current_url + u"?" + encoded
+        return current_url
+
+    def getSubPageByName(self, subpage_name, parent=None):
+        """retrieve a subpage and its path using its name
+
+        @param subpage_name(unicode): name of the sub page
+            it must be a direct children of parent page
+        @param parent(LiberviaPage, None): parent page
+            None to use current page
+        @return (tuple[str, LiberviaPage]): page subpath and instance
+        @raise exceptions.NotFound: no page has been found
+        """
+        if parent is None:
+            parent = self
+        for path, child in parent.children.iteritems():
+            try:
+                child_name = child.name
+            except AttributeError:
+                #  LiberviaPages have a name, but maybe this is an other Resource
+                continue
+            if child_name == subpage_name:
+                return path, child
+        raise exceptions.NotFound(_(u"requested sub page has not been found"))
+
+    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
+            if an arg is None, it will be ignored
+        @return (unicode): absolute URL to the sub page
+        """
+        current_url = self.getCurrentURL(request)
+        path, child = self.getSubPageByName(page_name)
+        return os.path.join(
+            u"/", current_url, path, *[quote(a) for a in args if a is not None]
+        )
+
+    def getURLByNames(self, named_path):
+        """retrieve URL from pages names and arguments
+
+        @param named_path(list[tuple[unicode, list[unicode]]]): path to the page as a list
+            of tuples of 2 items:
+                - first item is page name
+                - second item is list of path arguments of this page
+        @return (unicode): URL to the requested page with given path arguments
+        @raise exceptions.NotFound: one of the page was not found
+        """
+        current_page = None
+        path = []
+        for page_name, page_args in named_path:
+            if current_page is None:
+                current_page = self.getPageByName(page_name)
+                path.append(current_page.getURL(*page_args))
+            else:
+                sub_path, current_page = self.getSubPageByName(
+                    page_name, parent=current_page
+                )
+                path.append(sub_path)
+                if page_args:
+                    path.extend([quote(a) for a in page_args])
+        return self.host.checkRedirection(u"/".join(path))
+
+    def getURLByPath(self, *args):
+        """generate URL by path
+
+        this method as a similar effect as getURLByNames, but it is more readable
+        by using SubPage to get pages instead of using tuples
+        @param *args: path element:
+            - if unicode, will be used as argument
+            - if util.SubPage instance, must be the name of a subpage
+        @return (unicode): generated path
+        """
+        args = list(args)
+        if not args:
+            raise ValueError("You must specify path elements")
+        # root page is the one needed to construct the base of the URL
+        # if first arg is not a SubPage instance, we use current page
+        if not isinstance(args[0], SubPage):
+            root = self
+        else:
+            root = self.getPageByName(args.pop(0))
+        # we keep track of current page to check subpage
+        current_page = root
+        url_elts = []
+        arguments = []
+        while True:
+            while args and not isinstance(args[0], SubPage):
+                arguments.append(quote(args.pop(0)))
+            if not url_elts:
+                url_elts.append(root.getURL(*arguments))
+            else:
+                url_elts.extend(arguments)
+            if not args:
+                break
+            else:
+                path, current_page = current_page.getSubPageByName(args.pop(0))
+                arguments = [path]
+        return self.host.checkRedirection(u"/".join(url_elts))
+
+    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 _filterPathValue(self, value, handler, name, request):
+        """Modify a path value according to handler (see [getPathArgs])"""
+        if handler in (u"@", u"@jid") and value == u"@":
+            value = None
+
+        if handler in (u"", u"@"):
+            if value is None:
+                return u""
+        elif handler in (u"jid", u"@jid"):
+            if value:
+                try:
+                    return jid.JID(value)
+                except RuntimeError:
+                    log.warning(_(u"invalid jid argument: {value}").format(value=value))
+                    self.pageError(request, C.HTTP_BAD_REQUEST)
+            else:
+                return u""
+        else:
+            return handler(self, value, name, request)
+
+        return value
+
+    def getPathArgs(self, request, names, min_args=0, **kwargs):
+        """get several path arguments at once
+
+        Arguments will be put in request data.
+        Missing arguments will have None value
+        @param names(list[unicode]): list of arguments to get
+        @param min_args(int): if less than min_args are found, PageError is used with C.HTTP_BAD_REQUEST
+            use 0 to ignore
+        @param **kwargs: special value or optional callback to use for arguments
+            names of the arguments must correspond to those in names
+            special values may be:
+                - '': use empty string instead of None when no value is specified
+                - '@': if value of argument is empty or '@', empty string will be used
+                - 'jid': value must be converted to jid.JID if it exists, else empty string is used
+                - '@jid': if value of arguments is empty or '@', empty string will be used, else it will be converted to jid
+        """
+        data = self.getRData(request)
+
+        for idx, name in enumerate(names):
+            if name[0] == u"*":
+                value = data[name[1:]] = []
+                while True:
+                    try:
+                        value.append(self.nextPath(request))
+                    except IndexError:
+                        idx -= 1
+                        break
+                    else:
+                        idx += 1
+            else:
+                try:
+                    value = data[name] = self.nextPath(request)
+                except IndexError:
+                    data[name] = None
+                    idx -= 1
+                    break
+
+        values_count = idx + 1
+        if values_count < min_args:
+            log.warning(
+                _(
+                    u"Missing arguments in URL (got {count}, expected at least {min_args})"
+                ).format(count=values_count, min_args=min_args)
+            )
+            self.pageError(request, C.HTTP_BAD_REQUEST)
+
+        for name in names[values_count:]:
+            data[name] = None
+
+        for name, handler in kwargs.iteritems():
+            if name[0] == "*":
+                data[name] = [
+                    self._filterPathValue(v, handler, name, request) for v in data[name]
+                ]
+            else:
+                data[name] = self._filterPathValue(data[name], handler, name, request)
+
+    ## Cache handling ##
+
+    def _setCacheHeaders(self, request, cache):
+        """Set ETag and Last-Modified HTTP headers, used for caching"""
+        request.setHeader("ETag", cache.hash)
+        last_modified = self.host.getHTTPDate(cache.created)
+        request.setHeader("Last-Modified", last_modified)
+
+    def _checkCacheHeaders(self, request, cache):
+        """Check if a cache condition is set on the request
+
+        if condition is valid, C.HTTP_NOT_MODIFIED is returned
+        """
+        etag_match = request.getHeader("If-None-Match")
+        if etag_match is not None:
+            if cache.hash == etag_match:
+                self.pageError(request, C.HTTP_NOT_MODIFIED, no_body=True)
+        else:
+            modified_match = request.getHeader("If-Modified-Since")
+            if modified_match is not None:
+                modified = date_utils.date_parse(modified_match)
+                if modified >= int(cache.created):
+                    self.pageError(request, C.HTTP_NOT_MODIFIED, no_body=True)
+
+    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_PUBSUB:
+                service: pubsub service
+                node: pubsub node
+                short: short name of feature (needed if node is empty to find namespace)
+
+        """
+        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][request.uri][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 = [self, profile, cache_type, service, node, request.uri]
+                #  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()
+        self._setCacheHeaders(request, cache)
+        self._checkCacheHeaders(request, cache)
+        request.write(cache.rendered)
+        request.finish()
+        raise failure.Failure(exceptions.CancelError(u"cache is used"))
+
+    def _cacheURL(self, dummy, request, profile):
+        self.cached_urls.setdefault(profile, {})[request.uri] = CacheURL(request)
+
+    @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()
+
+    @classmethod
+    def onSignal(cls, host, signal, *args):
+        """Generic method which receive registered signals
+
+        if a callback is registered for this signal, call it
+        @param host: Libervia instance
+        @param signal(unicode): name of the signal
+        @param *args: args of the signals
+        """
+        for page, request, check_profile in cls.signals_handlers.get(
+            signal, {}
+        ).itervalues():
+            if check_profile:
+                signal_profile = args[-1]
+                request_profile = page.getProfile(request)
+                if not request_profile:
+                    # if you want to use signal without session, unset check_profile
+                    # (be sure to know what you are doing)
+                    log.error(_(u"no session started, signal can't be checked"))
+                    continue
+                if signal_profile != request_profile:
+                    #  we ignore the signal, it's not for our profile
+                    continue
+            if request._signals_cache is not None:
+                # socket is not yet opened, we cache the signal
+                request._signals_cache.append((request, signal, args))
+                log.debug(
+                    u"signal [{signal}] cached: {args}".format(signal=signal, args=args)
+                )
+            else:
+                page.on_signal(page, request, signal, *args)
+
+    def onSocketOpen(self, request):
+        """Called for dynamic pages when socket has just been opened
+
+        we send all cached signals
+        """
+        assert request._signals_cache is not None
+        cache = request._signals_cache
+        request._signals_cache = None
+        for request, signal, args in cache:
+            self.on_signal(self, request, signal, *args)
+
+    def onSocketClose(self, request):
+        """Called for dynamic pages when socket has just been closed
+
+        we remove signal handler
+        """
+        for signal in request._signals_registered:
+            try:
+                del LiberviaPage.signals_handlers[signal][id(request)]
+            except KeyError:
+                log.error(
+                    _(
+                        u"Can't find signal handler for [{signal}], this should not happen"
+                    ).format(signal=signal)
+                )
+            else:
+                log.debug(_(u"Removed signal handler"))
+
+    def delegateToResource(self, request, resource):
+        """continue workflow with Twisted Resource"""
+        buf = resource.render(request)
+        if buf == server.NOT_DONE_YET:
+            pass
+        else:
+            request.write(buf)
+            request.finish()
+        raise failure.Failure(exceptions.CancelError(u"resource delegation"))
+
+    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 = request.args["redirect_url"][0]
+        except (KeyError, IndexError):
+            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, path_args=None):
+        """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
+        @param path_args(list[unicode], None): path arguments to use in redirected page
+        @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]
+
+        if path_args is not None:
+            args = [quote(a) for a in path_args]
+            request.postpath = args + request.postpath
+
+        if self._do_cache:
+            # if cache is needed, it will be handled by final page
+            redirect_page._do_cache = self._do_cache
+            self._do_cache = None
+
+        redirect_page.renderPage(request, skip_parse_url=skip_parse_url)
+        raise failure.Failure(exceptions.CancelError(u"page redirection is used"))
+
+    def pageError(self, request, code=C.HTTP_NOT_FOUND, no_body=False):
+        """generate an error page and terminate the request
+
+        @param request(server.Request): HTTP request
+        @param core(int): error code to use
+        @param no_body: don't write body if True
+        """
+        request.setResponseCode(code)
+        if no_body:
+            request.finish()
+        else:
+            template = u"error/" + unicode(code) + ".html"
+
+            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")
+
+        if self._do_cache is not None:
+            redirected_page = self._do_cache.pop(0)
+            cache = reduce(lambda d, k: d.setdefault(k, {}), self._do_cache, self.cache)
+            page_cache = cache[redirected_page] = CachePage(data_encoded)
+            self._setCacheHeaders(request, page_cache)
+            log.debug(
+                _(u"{page} put in cache for [{profile}]").format(
+                    page=self, profile=self._do_cache[0]
+                )
+            )
+            self._do_cache = None
+            self._checkCacheHeaders(request, page_cache)
+
+        request.write(data_encoded)
+        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_dynamic(self, dummy, request):
+        # we need to activate dynamic page
+        # we set data for template, and create/register token
+        socket_token = unicode(uuid.uuid4())
+        socket_url = self.host.getWebsocketURL(request)
+        socket_debug = C.boolConst(self.host.debug)
+        request.template_data["websocket"] = WebsocketMeta(
+            socket_url, socket_token, socket_debug
+        )
+        self.host.registerWSToken(socket_token, self, request)
+        # we will keep track of handlers to remove
+        request._signals_registered = []
+        # we will cache registered signals until socket is opened
+        request._signals_cache = []
+
+    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,
+            cache_path=session_data.cache_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 or from URL's query part 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
+        """
+        #  FIXME: request.args is already unquoting the value, it seems we are doing double unquote
+        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_=(), multiple=True):
+        """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
+        @param multiple(bool): if False, only the first values are returned
+        @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
+            if not multiple:
+                ret[key] = urllib.unquote(values[0]).decode("utf-8")
+            else:
+                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 renderPartial(self, request, template, template_data):
+        """Render a template to be inserted in dynamic page
+
+        this is NOT the normal page rendering method, it is used only to update
+        dynamic pages
+        @param template(unicode): path of the template to render
+        @param template_data(dict): template_data to use
+        """
+        if not self.dynamic:
+            raise exceptions.InternalError(
+                _(u"renderPartial must only be used with dynamic pages")
+            )
+        session_data = self.host.getSessionData(request, session_iface.ISATSession)
+
+        return self.host.renderer.render(
+            template,
+            root_path="/templates/",
+            media_path="/" + C.MEDIA_DIR,
+            cache_path=session_data.cache_dir,
+            main_menu=LiberviaPage.main_menu,
+            **template_data
+        )
+
+    def renderAndUpdate(
+        self, request, template, selectors, template_data_update, update_type="append"
+    ):
+        """Helper method to render a partial page element and update the page
+
+        this is NOT the normal page rendering method, it is used only to update
+        dynamic pages
+        @param request(server.Request): current HTTP request
+        @param template: same as for [renderPartial]
+        @param selectors: CSS selectors to use
+        @param template_data_update: template data to use
+            template data cached in request will be copied then updated
+            with this data
+        @parap update_type(unicode): one of:
+            append: append rendered element to selected element
+        """
+        template_data = request.template_data.copy()
+        template_data.update(template_data_update)
+        html = self.renderPartial(request, template, template_data)
+        request.sendData(u"dom", selectors=selectors, update_type=update_type, html=html)
+
+    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:
+            d.addCallback(
+                lambda dummy: self.pageRedirect(
+                    self.redirect, request, skip_parse_url=False
+                )
+            )
+
+        if self.parse_url is not None and not skip_parse_url:
+            if self.url_cache:
+                profile = self.getProfile(request)
+                try:
+                    cache_url = self.cached_urls[profile][request.uri]
+                except KeyError:
+                    # no cache for this URI yet
+                    #  we do normal URL parsing, and then the cache
+                    d.addCallback(self.parse_url, request)
+                    d.addCallback(self._cacheURL, request, profile)
+                else:
+                    log.debug(_(u"using URI cache for {page}").format(page=self))
+                    cache_url.use(request)
+            else:
+                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.dynamic:
+            d.addCallback(self._prepare_dynamic, request)
+
+        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)