changeset 1487:579250dfe6d0

merge bookmark 0.9
author Goffi <goffi@goffi.org>
date Fri, 03 Dec 2021 17:43:07 +0100
parents 6ad203673853 (current diff) 6643855770a5 (diff)
children 42f110cab605
files libervia/VERSION
diffstat 10 files changed, 39 insertions(+), 908 deletions(-) [+]
line wrap: on
line diff
--- a/libervia/VERSION	Tue Nov 30 23:35:25 2021 +0100
+++ b/libervia/VERSION	Fri Dec 03 17:43:07 2021 +0100
@@ -1,1 +1,1 @@
-0.8.0
+0.9.0D
--- a/libervia/pages/blog/view/page_meta.py	Tue Nov 30 23:35:25 2021 +0100
+++ b/libervia/pages/blog/view/page_meta.py	Fri Dec 03 17:43:07 2021 +0100
@@ -128,7 +128,9 @@
                                       node,
                                       C.NO_LIMIT,
                                       [],
-                                      {C.KEY_ORDER_BY: C.ORDER_BY_CREATION},
+                                      data_format.serialise(
+                                          {C.KEY_ORDER_BY: C.ORDER_BY_CREATION}
+                                      ),
                                       profile)
             except Exception as e:
                 log.warning(
@@ -162,7 +164,7 @@
                               node,
                               C.NO_LIMIT,
                               items_id,
-                              extra,
+                              data_format.serialise(extra),
                               profile)
     except Exception as e:
         # FIXME: need a better way to test errors in bridge errback
--- a/libervia/pages/lists/view/page_meta.py	Tue Nov 30 23:35:25 2021 +0100
+++ b/libervia/pages/lists/view/page_meta.py	Fri Dec 03 17:43:07 2021 +0100
@@ -82,7 +82,7 @@
         C.NO_LIMIT,
         [],
         "",
-        extra,
+        data_format.serialise(extra),
         profile,
     )
     if profile != C.SERVICE_PROFILE:
--- a/libervia/pages/lists/view_item/page_meta.py	Tue Nov 30 23:35:25 2021 +0100
+++ b/libervia/pages/lists/view_item/page_meta.py	Fri Dec 03 17:43:07 2021 +0100
@@ -80,7 +80,8 @@
             template_data["comments_service"] = comments_service = uri_data["path"]
             try:
                 comments = data_format.deserialise(await self.host.bridgeCall(
-                    "mbGet", comments_service, comments_node, C.NO_LIMIT, [], {}, profile
+                    "mbGet", comments_service, comments_node, C.NO_LIMIT, [],
+                    data_format.serialise({}), profile
                 ))
             except BridgeException as e:
                 if e.classname == 'NotFound' or e.condition == 'item-not-found':
--- a/libervia/pages/merge-requests/view/page_meta.py	Tue Nov 30 23:35:25 2021 +0100
+++ b/libervia/pages/merge-requests/view/page_meta.py	Fri Dec 03 17:43:07 2021 +0100
@@ -65,7 +65,8 @@
         template_data["comments_node"] = comments_node = uri_data["node"]
         template_data["comments_service"] = comments_service = uri_data["path"]
         template_data["comments"] = data_format.deserialise(await self.host.bridgeCall(
-            "mbGet", comments_service, comments_node, C.NO_LIMIT, [], {}, profile
+            "mbGet", comments_service, comments_node, C.NO_LIMIT, [],
+            data_format.serialise({}), profile
         ))
 
         template_data["login_url"] = self.getPageRedirectURL(request)
--- a/libervia/server/blog.py	Tue Nov 30 23:35:25 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,897 +0,0 @@
-#!/usr/bin/env python3
-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2021 Jérôme Poisson <goffi@goffi.org>
-# Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.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 sat.core.i18n import _, D_
-from sat_frontends.tools.strings import addURLToText, fixXHTMLLinks
-from sat.core.log import getLogger
-
-log = getLogger(__name__)
-from sat.tools.common import data_format
-from sat.tools import xml_tools
-from dbus.exceptions import DBusException
-from twisted.internet import defer
-from twisted.web import server
-from twisted.web.resource import Resource
-from twisted.words.protocols.jabber.jid import JID
-from twisted.words.xish import domish
-from jinja2 import Environment, PackageLoader
-from datetime import datetime
-import re
-import os
-import sys
-import urllib.request, urllib.parse, urllib.error
-
-from libervia.server.html_tools import sanitizeHtml, convertNewLinesToXHTML
-from libervia.server.constants import Const as C
-
-NS_ATOM = "http://www.w3.org/2005/Atom"
-PARAMS_TO_GET = (
-    C.STATIC_BLOG_PARAM_TITLE,
-    C.STATIC_BLOG_PARAM_BANNER,
-    C.STATIC_BLOG_PARAM_KEYWORDS,
-    C.STATIC_BLOG_PARAM_DESCRIPTION,
-)
-re_strip_empty_div = re.compile(r"<div ?/>|<div> *?</div>")
-
-# TODO: check disco features and use max_items when RSM is not available
-# FIXME: change navigation links handling, this is is fragile
-# XXX: this page will disappear, LiberviaPage will be used instead
-# TODO: delete this page and create a compatibility page for links
-
-
-def getDefaultQueryData(request):
-    """Return query data which must be present in all links
-
-    @param request(twisted.web.http.Request): request instance comming from render
-    @return (dict): a dict with values as expected by urllib.urlencode
-    """
-    default_query_data = {}
-    try:
-        default_query_data["tag"] = request.extra_dict[
-            "mam_filter_{}".format(C.MAM_FILTER_CATEGORY)
-        ].encode("utf-8")
-    except KeyError:
-        pass
-    return default_query_data
-
-
-def _quote(value):
-    """Quote a value for use in url
-
-    @param value(unicode): value to quote
-    @return (str): quoted value
-    """
-    return urllib.parse.quote(value.encode("utf-8"), "")
-
-
-def _unquote(quoted_value):
-    """Unquote a value coming from url
-
-    @param unquote_value(str): value to unquote
-    @return (unicode): unquoted value
-    """
-    assert not isinstance(quoted_value, str)
-    return urllib.parse.unquote(quoted_value).decode("utf-8")
-
-
-def _urlencode(query):
-    """Same as urllib.urlencode, but use '&amp;' instead of '&'"""
-    return "&amp;".join(
-        [
-            "{}={}".format(urllib.parse.quote_plus(str(k)), urllib.parse.quote_plus(str(v)))
-            for k, v in query.items()
-        ]
-    )
-
-
-class TemplateProcessor(object):
-
-    THEME = "default"
-
-    def __init__(self, host):
-        self.host = host
-
-        # add Libervia's themes directory to the python path
-        sys.path.append(os.path.dirname(os.path.normpath(self.host.themes_dir)))
-        themes = os.path.basename(os.path.normpath(self.host.themes_dir))
-        self.env = Environment(loader=PackageLoader(themes, self.THEME))
-
-    def useTemplate(self, request, tpl, data=None):
-        theme_url = os.path.join("/", C.THEMES_URL, self.THEME)
-
-        data_ = {
-            "images": os.path.join(theme_url, "images"),
-            "styles": os.path.join(theme_url, "styles"),
-        }
-        if data:
-            data_.update(data)
-
-        template = self.env.get_template("{}.html".format(tpl))
-        return template.render(**data_).encode("utf-8")
-
-
-class MicroBlog(Resource, TemplateProcessor):
-    isLeaf = True
-
-    def __init__(self, host):
-        self.host = host
-        Resource.__init__(self)
-        TemplateProcessor.__init__(self, host)
-        self.avatars_cache = {}
-
-    def _avatarPathToUrl(self, avatar, request, bare_jid_s):
-        filename = os.path.basename(avatar)
-        avatar_url = os.path.join(self.host.service_cache_url, filename)
-        self.avatars_cache[bare_jid_s] = avatar_url
-        return avatar_url
-
-    def getAvatarURL(self, pub_jid, request):
-        """Return avatar of a jid if in cache, else ask for it.
-
-        @param pub_jid (JID): publisher JID
-        @return: deferred avatar URL (unicode)
-        """
-        bare_jid_s = pub_jid.userhost()
-        try:
-            url = self.avatars_cache[bare_jid_s]
-        except KeyError:
-            self.avatars_cache[
-                bare_jid_s
-            ] = ""  # avoid to request the vcard several times
-            d = self.host.bridgeCall(
-                "avatarGet", bare_jid_s, False, False, C.SERVICE_PROFILE
-            )
-            d.addCallback(self._avatarPathToUrl, request, bare_jid_s)
-            return d
-        return defer.succeed(url if url else C.DEFAULT_AVATAR_URL)
-
-    def render_GET(self, request):
-        if not request.postpath or len(request.postpath) > 2:
-            return self.useTemplate(
-                request, "static_blog_error", {"message": "You must indicate a nickname"}
-            )
-
-        prof_requested = _unquote(request.postpath[0])
-
-        try:
-            prof_found = self.host.bridge.profileNameGet(prof_requested)
-        except DBusException:
-            prof_found = None
-        if not prof_found or prof_found == C.SERVICE_PROFILE:
-            return self.useTemplate(
-                request, "static_blog_error", {"message": "Invalid nickname"}
-            )
-
-        d = defer.Deferred()
-        # TODO: jid caching
-        self.host.bridge.asyncGetParamA(
-            "JabberID",
-            "Connection",
-            "value",
-            profile_key=prof_found,
-            callback=d.callback,
-            errback=d.errback,
-        )
-        d.addCallback(self.render_gotJID, request, prof_found)
-        return server.NOT_DONE_YET
-
-    def render_gotJID(self, pub_jid_s, request, profile):
-        pub_jid = JID(pub_jid_s)
-
-        request.extra_dict = {}  # will be used for RSM and MAM
-        self.parseURLParams(request)
-        if request.item_id:
-            # FIXME: this part seems useless
-            # we want a specific item
-            # item_ids = [request.item_id]
-            # max_items = 1
-            max_items = C.NO_LIMIT  # FIXME
-        else:
-            # max_items = int(request.extra_dict['rsm_max']) # FIXME
-            max_items = C.NO_LIMIT
-            # TODO: use max_items only when RSM is not available
-
-        if request.atom:
-            request.extra_dict.update(request.mam_extra)
-            self.getAtom(
-                pub_jid,
-                max_items,
-                request.extra_dict,
-                request.extra_comments_dict,
-                request,
-                profile,
-            )
-
-        elif request.item_id:
-            # we can't merge mam_extra now because we'll use item_ids
-            self.getItemById(
-                pub_jid,
-                request.item_id,
-                request.extra_dict,
-                request.extra_comments_dict,
-                request,
-                profile,
-            )
-        else:
-            request.extra_dict.update(request.mam_extra)
-            self.getItems(
-                pub_jid,
-                max_items,
-                request.extra_dict,
-                request.extra_comments_dict,
-                request,
-                profile,
-            )
-
-    ## URL parsing
-
-    def parseURLParams(self, request):
-        """Parse the request URL parameters.
-
-        @param request: HTTP request
-        """
-        if len(request.postpath) > 1:
-            if request.postpath[1] == "atom.xml":  # return the atom feed
-                request.atom = True
-                request.item_id = None
-            else:
-                request.atom = False
-                request.item_id = _unquote(request.postpath[1])
-        else:
-            request.item_id = None
-            request.atom = False
-
-        self.parseURLParamsRSM(request)
-        # XXX: request.display_single is True when only one blog post is visible
-        request.display_single = (request.item_id is not None) or int(
-            request.extra_dict["rsm_max"]
-        ) == 1
-        self.parseURLParamsCommentsRSM(request)
-        self.parseURLParamsMAM(request)
-
-    def parseURLParamsRSM(self, request):
-        """Parse RSM request data from the URL parameters for main items
-
-        fill request.extra_dict accordingly
-        @param request: HTTP request
-        """
-        if request.item_id:  # XXX: item_id and RSM are not compatible
-            return
-        try:
-            rsm_max = int(request.args["max"][0])
-            if rsm_max > C.STATIC_RSM_MAX_LIMIT:
-                log.warning("Request with rsm_max over limit ({})".format(rsm_max))
-                rsm_max = C.STATIC_RSM_MAX_LIMIT
-            request.extra_dict["rsm_max"] = str(rsm_max)
-        except (ValueError, KeyError):
-            request.extra_dict["rsm_max"] = str(C.STATIC_RSM_MAX_DEFAULT)
-        try:
-            request.extra_dict["rsm_index"] = request.args["index"][0]
-        except (ValueError, KeyError):
-            try:
-                request.extra_dict["rsm_before"] = request.args["before"][0].decode(
-                    "utf-8"
-                )
-            except KeyError:
-                try:
-                    request.extra_dict["rsm_after"] = request.args["after"][0].decode(
-                        "utf-8"
-                    )
-                except KeyError:
-                    pass
-
-    def parseURLParamsCommentsRSM(self, request):
-        """Parse RSM request data from the URL parameters for comments
-
-        fill request.extra_dict accordingly
-        @param request: HTTP request
-        """
-        request.extra_comments_dict = {}
-        if request.display_single:
-            try:
-                rsm_max = int(request.args["comments_max"][0])
-                if rsm_max > C.STATIC_RSM_MAX_LIMIT:
-                    log.warning("Request with rsm_max over limit ({})".format(rsm_max))
-                    rsm_max = C.STATIC_RSM_MAX_LIMIT
-                request.extra_comments_dict["rsm_max"] = str(rsm_max)
-            except (ValueError, KeyError):
-                request.extra_comments_dict["rsm_max"] = str(
-                    C.STATIC_RSM_MAX_COMMENTS_DEFAULT
-                )
-        else:
-            request.extra_comments_dict["rsm_max"] = "0"
-
-    def parseURLParamsMAM(self, request):
-        """Parse MAM request data from the URL parameters for main items
-
-        fill request.extra_dict accordingly
-        @param request: HTTP request
-        """
-        # XXX: we use a separate dict for MAM as the filters are not used
-        #      when display_single is set (because it then use item_ids which
-        #      can't be used with MAM), but it is still used in this case
-        #      for navigation links.
-        request.mam_extra = {}
-        try:
-            request.mam_extra[
-                "mam_filter_{}".format(C.MAM_FILTER_CATEGORY)
-            ] = request.args["tag"][0].decode("utf-8")
-        except KeyError:
-            pass
-
-    ## Items retrieval
-
-    def getItemById(
-        self, pub_jid, item_id, extra_dict, extra_comments_dict, request, profile
-    ):
-        """
-
-        @param pub_jid (jid.JID): publisher JID
-        @param item_id(unicode): ID of the item to retrieve
-        @param extra_dict (dict): extra configuration for initial items only
-        @param extra_comments_dict (dict): extra configuration for comments only
-        @param request: HTTP request
-        @param profile
-        """
-
-        def gotItems(items):
-            items, metadata = items
-            items = [data_format.deserialise(i) for i in items]
-            item = items[0]  # assume there's only one item
-
-            def gotMetadata(result):
-                __, rsm_metadata = result
-                try:
-                    metadata["rsm_count"] = rsm_metadata["rsm_count"]
-                except KeyError:
-                    pass
-                try:
-                    metadata["rsm_index"] = str(int(rsm_metadata["rsm_index"]) - 1)
-                except KeyError:
-                    pass
-
-                metadata["rsm_first"] = metadata["rsm_last"] = item["id"]
-
-                def gotComments(comments):
-                    # at this point we can merge mam dict
-                    request.extra_dict.update(request.mam_extra)
-                    # build the items as self.getItems would do it (and as self.renderHTML expects them to be)
-                    comments = [
-                        (
-                            item["comments_service"],
-                            item["comments_node"],
-                            "",
-                            [data_format.deserialise(c) for c in comments[0]],
-                            comments[1],
-                        )
-                    ]
-                    self.renderHTML(
-                        [(item, comments)], metadata, request, pub_jid, profile
-                    )
-
-                # get the comments
-                # max_comments = int(extra_comments_dict['rsm_max']) # FIXME
-                max_comments = C.NO_LIMIT
-                # TODO: use max_comments only when RSM is not available
-                self.host.bridge.mbGet(
-                    item["comments_service"],
-                    item["comments_node"],
-                    max_comments,
-                    [],
-                    extra_comments_dict,
-                    C.SERVICE_PROFILE,
-                    callback=gotComments,
-                    errback=lambda failure: self.renderError(failure, request, pub_jid),
-                )
-
-            # XXX: retrieve RSM information related to the main item. We can't do it while
-            # retrieving the item, because item_ids and rsm should not be used together.
-            self.host.bridge.mbGet(
-                pub_jid.userhost(),
-                "",
-                0,
-                [],
-                {"rsm_max": "1", "rsm_after": item["id"]},
-                C.SERVICE_PROFILE,
-                callback=gotMetadata,
-                errback=lambda failure: self.renderError(failure, request, pub_jid),
-            )
-
-        # get the main item
-        self.host.bridge.mbGet(
-            pub_jid.userhost(),
-            "",
-            0,
-            [item_id],
-            extra_dict,
-            C.SERVICE_PROFILE,
-            callback=gotItems,
-            errback=lambda failure: self.renderError(failure, request, pub_jid),
-        )
-
-    def getItems(
-        self, pub_jid, max_items, extra_dict, extra_comments_dict, request, profile
-    ):
-        """
-
-        @param pub_jid (jid.JID): publisher JID
-        @param max_items(int): maximum number of item to get, C.NO_LIMIT for no limit
-        @param extra_dict (dict): extra configuration for initial items only
-        @param extra_comments_dict (dict): extra configuration for comments only
-        @param request: HTTP request
-        @param profile
-        """
-
-        def getResultCb(data, rt_session):
-            remaining, results = data
-            # we have requested one node only
-            assert remaining == 0
-            assert len(results) == 1
-            service, node, failure, items, metadata = results[0]
-            items = [(data_format.deserialise(i), m) for i,m in items]
-            if failure:
-                self.renderError(failure, request, pub_jid)
-            else:
-                self.renderHTML(items, metadata, request, pub_jid, profile)
-
-        def getResult(rt_session):
-            self.host.bridge.mbGetFromManyWithCommentsRTResult(
-                rt_session,
-                C.SERVICE_PROFILE,
-                callback=lambda data: getResultCb(data, rt_session),
-                errback=lambda failure: self.renderError(failure, request, pub_jid),
-            )
-
-        # max_comments = int(extra_comments_dict['rsm_max']) # FIXME
-        max_comments = 0
-        # TODO: use max_comments only when RSM is not available
-        self.host.bridge.mbGetFromManyWithComments(
-            C.JID,
-            [pub_jid.userhost()],
-            max_items,
-            max_comments,
-            extra_dict,
-            extra_comments_dict,
-            C.SERVICE_PROFILE,
-            callback=getResult,
-        )
-
-    def getAtom(
-        self, pub_jid, max_items, extra_dict, extra_comments_dict, request, profile
-    ):
-        """
-
-        @param pub_jid (jid.JID): publisher JID
-        @param max_items(int): maximum number of item to get, C.NO_LIMIT for no limit
-        @param extra_dict (dict): extra configuration for initial items only
-        @param extra_comments_dict (dict): extra configuration for comments only
-        @param request: HTTP request
-        @param profile
-        """
-
-        def gotItems(data):
-            # Generate a clean atom feed with uri linking to this blog
-            # from microblog data
-            items, metadata = data
-            items = [data_format.deserialise(i) for i in items]
-            feed_elt = domish.Element((NS_ATOM, "feed"))
-            title = _("{user}'s blog").format(user=profile)
-            feed_elt.addElement("title", content=title)
-
-            base_blog_url = self.host.getExtBaseURL(
-                request, "blog/{user}".format(user=profile)
-            )
-
-            # atom link
-            link_feed_elt = feed_elt.addElement("link")
-            link_feed_elt["href"] = "{base}/atom.xml".format(base=base_blog_url)
-            link_feed_elt["type"] = "application/atom+xml"
-            link_feed_elt["rel"] = "self"
-
-            # blog link
-            link_blog_elt = feed_elt.addElement("link")
-            link_blog_elt["rel"] = "alternate"
-            link_blog_elt["type"] = "text/html"
-            link_blog_elt["href"] = base_blog_url
-
-            # blog link XMPP uri
-            blog_xmpp_uri = metadata["uri"]
-            link_blog_elt = feed_elt.addElement("link")
-            link_blog_elt["rel"] = "alternate"
-            link_blog_elt["type"] = "application/atom+xml"
-            link_blog_elt["href"] = blog_xmpp_uri
-
-            feed_elt.addElement("id", content=_quote(blog_xmpp_uri))
-            updated_unix = max([float(item["updated"]) for item in items])
-            updated_dt = datetime.fromtimestamp(updated_unix)
-            feed_elt.addElement(
-                "updated", content="{}Z".format(updated_dt.isoformat("T"))
-            )
-
-            for item in items:
-                entry_elt = feed_elt.addElement("entry")
-
-                # Title
-                try:
-                    title = item["title"]
-                except KeyError:
-                    # for microblog (without title), we use an abstract of content as title
-                    title = "{}…".format(" ".join(item["content"][:70].split()))
-                entry_elt.addElement("title", content=title)
-
-                # HTTP link
-                http_link_elt = entry_elt.addElement("link")
-                http_link_elt["rel"] = "alternate"
-                http_link_elt["type"] = "text/html"
-                http_link_elt["href"] = "{base}/{quoted_id}".format(
-                    base=base_blog_url, quoted_id=_quote(item["id"])
-                )
-                # XMPP link
-                xmpp_link_elt = entry_elt.addElement("link")
-                xmpp_link_elt["rel"] = "alternate"
-                xmpp_link_elt["type"] = "application/atom+xml"
-                xmpp_link_elt["href"] = "{blog_uri};item={item_id}".format(
-                    blog_uri=blog_xmpp_uri, item_id=item["id"]
-                )
-
-                # date metadata
-                entry_elt.addElement("id", content=item["atom_id"])
-                updated = datetime.fromtimestamp(float(item["updated"]))
-                entry_elt.addElement(
-                    "updated", content="{}Z".format(updated.isoformat("T"))
-                )
-                published = datetime.fromtimestamp(float(item["published"]))
-                entry_elt.addElement(
-                    "published", content="{}Z".format(published.isoformat("T"))
-                )
-
-                # author metadata
-                author_elt = entry_elt.addElement("author")
-                author_elt.addElement("name", content=item.get("author", profile))
-                try:
-                    author_elt.addElement(
-                        "uri", content="xmpp:{}".format(item["author_jid"])
-                    )
-                except KeyError:
-                    pass
-                try:
-                    author_elt.addElement("email", content=item["author_email"])
-                except KeyError:
-                    pass
-
-                # categories
-                for tag in item.get('tags', []):
-                    category_elt = entry_elt.addElement("category")
-                    category_elt["term"] = tag
-
-                # content
-                try:
-                    content_xhtml = item["content_xhtml"]
-                except KeyError:
-                    content_elt = entry_elt.addElement("content", content="content")
-                    content_elt["type"] = "text"
-                else:
-                    content_elt = entry_elt.addElement("content")
-                    content_elt["type"] = "xhtml"
-                    content_elt.addChild(
-                        xml_tools.ElementParser()(content_xhtml, namespace=C.NS_XHTML)
-                    )
-
-            atom_feed = '<?xml version="1.0" encoding="utf-8"?>\n{}'.format(
-                feed_elt.toXml()
-            )
-            self.renderAtomFeed(atom_feed, request),
-
-        self.host.bridge.mbGet(
-            pub_jid.userhost(),
-            "",
-            max_items,
-            [],
-            extra_dict,
-            C.SERVICE_PROFILE,
-            callback=gotItems,
-        )
-
-    ## rendering
-
-    def _updateDict(self, value, dict_, key):
-        dict_[key] = value
-
-    def _getImageParams(self, options, key, default, alt):
-        """regexp from http://answers.oreilly.com/topic/280-how-to-validate-urls-with-regular-expressions/"""
-        url = options[key] if key in options else ""
-        regexp = (
-            r"^(https?|ftp)://[a-z0-9-]+(\.[a-z0-9-]+)+(/[\w-]+)*/[\w-]+\.(gif|png|jpg)$"
-        )
-        if re.match(regexp, url):
-            url = url
-        else:
-            url = default
-        return BlogImage(url, alt)
-
-    def renderError(self, failure, request, pub_jid):
-        request.setResponseCode(500)
-        request.write(
-            self.useTemplate(
-                request, "static_blog_error", {"message": "Can't access requested data"}
-            )
-        )
-        request.finish()
-
-    def renderHTML(self, items, metadata, request, pub_jid, profile):
-        """Retrieve the user parameters before actually rendering the static blog
-
-        @param items(list[tuple(dict, list)]): same as in self._renderHTML
-        @param metadata(dict): original node metadata
-        @param request: HTTP request
-        @param pub_jid (JID): publisher JID
-        @param profile (unicode): %(doc_profile)s
-        """
-        d_list = []
-        options = {}
-
-        d = self.getAvatarURL(pub_jid, request)
-        d.addCallback(self._updateDict, options, "avatar")
-        d.addErrback(self.renderError, request, pub_jid)
-        d_list.append(d)
-
-        for param_name in PARAMS_TO_GET:
-            d = defer.Deferred()
-            self.host.bridge.asyncGetParamA(
-                param_name,
-                C.STATIC_BLOG_KEY,
-                "value",
-                C.SERVER_SECURITY_LIMIT,
-                profile,
-                callback=d.callback,
-                errback=d.errback,
-            )
-            d.addCallback(self._updateDict, options, param_name)
-            d.addErrback(self.renderError, request, pub_jid)
-            d_list.append(d)
-
-        dlist_d = defer.DeferredList(d_list)
-        dlist_d.addCallback(
-            lambda __: self._renderHTML(items, metadata, options, request, pub_jid)
-        )
-
-    def _renderHTML(self, items, metadata, options, request, pub_jid):
-        """Actually render the static blog.
-
-        If mblog_data is a list of dict, we are missing the comments items so we just
-        display the main items. If mblog_data is a list of couple, each couple is
-        associating a main item data with the list of its comments, so we render all.
-        @param items(list[tuple(dict, list)]): list of 2-tuple with
-            - item(dict): item microblog data
-            - comments_list(list[tuple]): list of 5-tuple with
-                - service (unicode): pubsub service where the comments node is
-                - node (unicode): comments node
-                - failure (unicode): empty in case of success, else error message
-                - comments(list[dict]): list of microblog data
-                - comments_metadata(dict): metadata of the comment node
-        @param metadata(dict): original node metadata
-        @param options: dict defining the blog's parameters
-        @param request: the HTTP request
-        @param pub_jid (JID): publisher JID
-        """
-        if not isinstance(options, dict):
-            options = {}
-        user = sanitizeHtml(pub_jid.user)
-        base_url = os.path.join("/blog/", user)
-
-        def getOption(key):
-            return sanitizeHtml(options[key]) if key in options else ""
-
-        avatar = os.path.normpath("/{}".format(getOption("avatar")))
-        title = getOption(C.STATIC_BLOG_PARAM_TITLE) or user
-        query_data = _urlencode(getDefaultQueryData(request)).decode("utf-8")
-
-        xmpp_uri = metadata["uri"]
-        if len(items) == 1:
-            # FIXME: that's really not a good way to get item id
-            #        this must be changed after static blog refactorisation
-            item_id = items[0][0]["id"]
-            xmpp_uri += ";item={}".format(_quote(item_id))
-
-        data = {
-            "url_base": base_url,
-            "xmpp_uri": xmpp_uri,
-            "url_query": "?{}".format(query_data) if query_data else "",
-            "keywords": getOption(C.STATIC_BLOG_PARAM_KEYWORDS),
-            "description": getOption(C.STATIC_BLOG_PARAM_DESCRIPTION),
-            "title": title,
-            "favicon": avatar,
-            "banner_img": self._getImageParams(
-                options, C.STATIC_BLOG_PARAM_BANNER, avatar, title
-            ),
-        }
-
-        data["navlinks"] = NavigationLinks(request, items, metadata, base_url)
-        data["messages"] = []
-        for item in items:
-            item, comments_list = item
-            comments, comments_count = [], 0
-            for node_comments in comments_list:
-                comments.extend(node_comments[3])
-                try:
-                    comments_count += int(node_comments[4]["rsm_count"])
-                except KeyError:
-                    pass
-            data["messages"].append(
-                BlogMessage(request, base_url, item, comments, comments_count)
-            )
-
-        request.write(self.useTemplate(request, "static_blog", data))
-        request.finish()
-
-    def renderAtomFeed(self, feed, request):
-        request.write(feed.encode("utf-8"))
-        request.finish()
-
-
-class NavigationLinks(object):
-    def __init__(self, request, items, metadata, base_url):
-        """Build the navigation links.
-
-        @param items (list): list of items
-        @param metadata (dict): rsm data
-        @param base_url (unicode): the base URL for this user's blog
-        @return: dict
-        """
-        # FIXME: this code must be refactorized, it is fragile
-        #        and difficult to maintain
-
-        # query data which must be present in all links
-        default_query_data = getDefaultQueryData(request)
-
-        # which links we need to display
-        if request.display_single:
-            links = ("later_message", "older_message")
-            # key must exist when using the template
-            self.later_messages = self.older_messages = ""
-        else:
-            links = ("later_messages", "older_messages")
-            self.later_message = self.older_message = ""
-
-        # now we set the links according to RSM
-        for key in links:
-            query_data = default_query_data.copy()
-
-            if key.startswith("later_message"):
-                try:
-                    index = int(metadata["rsm_index"])
-                except (KeyError, ValueError):
-                    pass
-                else:
-                    if index == 0:
-                        # we don't show this link on first page
-                        setattr(self, key, "")
-                        continue
-                try:
-                    query_data["before"] = metadata["rsm_first"].encode("utf-8")
-                except KeyError:
-                    pass
-            else:
-                try:
-                    index = int(metadata["rsm_index"])
-                    count = int(metadata.get("rsm_count"))
-                except (KeyError, ValueError):
-                    # XXX: if we don't have index or count, we can't know if we
-                    #      are on the last page or not
-                    pass
-                else:
-                    # if we have index, we don't show the after link
-                    # on the last page
-                    if index + len(items) >= count:
-                        setattr(self, key, "")
-                        continue
-                try:
-                    query_data["after"] = metadata["rsm_last"].encode("utf-8")
-                except KeyError:
-                    pass
-
-            if request.display_single:
-                query_data["max"] = 1
-
-            link = "{}?{}".format(base_url, _urlencode(query_data))
-            setattr(self, key, BlogLink(link, key, key.replace("_", " ")))
-
-
-class BlogImage(object):
-    def __init__(self, url_, alt):
-        self.url = url_
-        self.alt = alt
-
-
-class BlogLink(object):
-    def __init__(self, url_, style, text):
-        self.url = url_
-        self.style = style
-        self.text = text
-
-
-class BlogMessage(object):
-    def __init__(self, request, base_url, entry, comments=None, comments_count=0):
-        """
-
-        @param request: HTTP request
-        @param base_url (unicode): the base URL
-        @param entry(dict): item microblog data
-        @param comments(list[dict]): list of microblog data
-        @param comments_count (int): total number of comments
-        """
-        if comments is None:
-            comments = []
-        timestamp = float(entry.get("published", 0))
-
-        # FIXME: for now we assume that the comments' depth is only 1
-        is_comment = not entry.get("comments", False)
-
-        self.date = datetime.fromtimestamp(timestamp)
-        self.type = "comment" if is_comment else "main_item"
-        self.style = "mblog_comment" if is_comment else ""
-        self.content = self.getText(entry, "content")
-
-        if is_comment:
-            self.author = _("from {}").format(entry["author"])
-        else:
-            self.author = "&nbsp;"
-            self.url = "{}/{}".format(base_url, _quote(entry["id"]))
-            query_data = getDefaultQueryData(request)
-            if query_data:
-                self.url += "?{}".format(_urlencode(query_data))
-            self.title = self.getText(entry, "title")
-            self.tags = [sanitizeHtml(tag) for tag in entry.get('tags', [])]
-
-            count_text = lambda count: D_("comments") if count > 1 else D_("comment")
-
-            self.comments_text = "{} {}".format(
-                comments_count, count_text(comments_count)
-            )
-
-            delta = comments_count - len(comments)
-            if request.display_single and delta > 0:
-                prev_url = "{}?{}".format(
-                    self.url, _urlencode({"comments_max": comments_count})
-                )
-                prev_text = D_("show {count} previous {comments}").format(
-                    count=delta, comments=count_text(delta)
-                )
-                self.all_comments_link = BlogLink(prev_url, "comments_link", prev_text)
-
-        if comments:
-            self.comments = [
-                BlogMessage(request, base_url, comment) for comment in comments
-            ]
-
-    def getText(self, entry, key):
-        try:
-            xhtml = entry["{}_xhtml".format(key)]
-        except KeyError:
-            try:
-                processor = addURLToText if key.startswith("content") else sanitizeHtml
-                return convertNewLinesToXHTML(processor(entry[key]))
-            except KeyError:
-                return None
-        else:
-            # FIXME: empty <div /> elements provoke rendering issue
-            #        this regex is a temporary workadound, need more investigation
-            xhtml = re_strip_empty_div.sub("", xhtml)
-            return fixXHTMLLinks(xhtml)
--- a/libervia/server/pages_tools.py	Tue Nov 30 23:35:25 2021 +0100
+++ b/libervia/server/pages_tools.py	Fri Dec 03 17:43:07 2021 +0100
@@ -1,7 +1,6 @@
 #!/usr/bin/env python3
 
-
-# Libervia: a Salut à Toi frontend
+# Libervia Web frontend
 # Copyright (C) 2011-2021 Jérôme Poisson <goffi@goffi.org>
 
 # This program is free software: you can redistribute it and/or modify
@@ -43,7 +42,9 @@
         else exception will be raised
     """
     try:
-        d = self.host.bridgeCall("mbGet", service, node, C.NO_LIMIT, [], {}, profile)
+        d = self.host.bridgeCall(
+            "mbGet", service, node, C.NO_LIMIT, [], data_format.serialise({}), profile
+        )
     except Exception as e:
         if not pass_exceptions:
             raise e
--- a/libervia/server/proxy.py	Tue Nov 30 23:35:25 2021 +0100
+++ b/libervia/server/proxy.py	Fri Dec 03 17:43:07 2021 +0100
@@ -52,7 +52,8 @@
         )
 
     def render(self, request):
-        # Forwarded and X-Forwarded-xxx headers can be set if we have behin an other proxy
+        # Forwarded and X-Forwarded-xxx headers can be set
+        # if we have behind an other proxy
         if ((not request.getHeader(C.H_FORWARDED)
              and not request.getHeader(C.H_X_FORWARDED_HOST))):
             forwarded_data = []
--- a/libervia/server/server.py	Tue Nov 30 23:35:25 2021 +0100
+++ b/libervia/server/server.py	Fri Dec 03 17:43:07 2021 +0100
@@ -517,6 +517,28 @@
 
                     # normal redirection system is not used here
                     continue
+                elif new_url.scheme == "proxy":
+                    # a reverse proxy
+                    host, port = new_url.hostname, new_url.port
+                    if host is None or port is None:
+                        raise ValueError(
+                            "invalid host or port in proxy redirection, please check your "
+                            "configuration: {new_url.geturl()}"
+                        )
+                    url_prefix = (new_url.path or old).rstrip('/')
+                    res = proxy.SatReverseProxyResource(
+                        host,
+                        port,
+                        url_prefix.encode(),
+                    )
+                    self.addResourceToPath(old, res)
+                    log.info(
+                        f"[{self.host_name}] Added redirection from /{old} to reverse proxy "
+                        f"{new_url.netloc} with URL prefix {url_prefix}/"
+                    )
+
+                    # normal redirection system is not used here
+                    continue
                 else:
                     raise NotImplementedError(
                         "{scheme}: scheme is not managed for url_redirections_dict".format(
--- a/setup.py	Tue Nov 30 23:35:25 2021 +0100
+++ b/setup.py	Fri Dec 03 17:43:07 2021 +0100
@@ -25,7 +25,7 @@
 DIR_NAME = "libervia"
 
 install_requires = [
-    "libervia-backend >=0.8.0b1, <0.9.0",
+    "libervia-backend == 0.9.*",
     "libervia-templates >=0.8.0b1, <0.9.0",
     'twisted[tls] >=20.3.0, <21.3.0',
     "zope.interface <5.5.0",