diff libervia/server/pages.py @ 1504:409d10211b20

server, browser: dynamic pages refactoring: dynamic pages has been reworked, to change the initial basic implementation. Pages are now dynamic by default, and a websocket is established by the first connected page of a session. The socket is used to transmit bridge signals, and then the signal is broadcasted to other tabs using broadcast channel. If the connecting tab is closed, an other one is chosen. Some tests are made to retry connecting in case of problem, and sometimes reload the pages (e.g. if profile is connected). Signals (or other data) are cached during reconnection phase, to avoid lost of data. All previous partial rendering mechanism have been removed, chat page is temporarily not working anymore, but will be eventually redone (one of the goal of this work is to have proper chat).
author Goffi <goffi@goffi.org>
date Wed, 01 Mar 2023 18:02:44 +0100
parents 1671d187e71d
children ce879da7fcf7
line wrap: on
line diff
--- a/libervia/server/pages.py	Wed Mar 01 17:55:25 2023 +0100
+++ b/libervia/server/pages.py	Wed Mar 01 18:02:44 2023 +0100
@@ -112,7 +112,6 @@
 
 class LiberviaPage(web_resource.Resource):
     isLeaf = True  #  we handle subpages ourself
-    signals_handlers = {}
     cache = {}
     #  Set of tuples (service/node/sub_id) of nodes subscribed for caching
     # sub_id can be empty string if not handled by service
@@ -120,9 +119,9 @@
 
     def __init__(
         self, host, vhost_root, root_dir, url, name=None, label=None, redirect=None,
-        access=None, dynamic=False, parse_url=None, add_breadcrumb=None,
+        access=None, dynamic=True, parse_url=None, add_breadcrumb=None,
         prepare_render=None, render=None, template=None, on_data_post=None, on_data=None,
-        on_signal=None, url_cache=False, replace_on_conflict=False
+        url_cache=False, replace_on_conflict=False
         ):
         """Initiate LiberviaPage instance
 
@@ -173,8 +172,6 @@
                     as a notification
         @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
         @param url_cache(boolean): if set, result of parse_url is cached (per profile).
             Useful when costly calls (e.g. network) are done while parsing URL.
         @param replace_on_conflict(boolean): if True, don't raise ConflictError if a
@@ -231,7 +228,6 @@
         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
@@ -305,7 +301,7 @@
             label=page_data.get("label"),
             redirect=page_data.get("redirect"),
             access=page_data.get("access"),
-            dynamic=page_data.get("dynamic", False),
+            dynamic=page_data.get("dynamic", True),
             parse_url=page_data.get("parse_url"),
             add_breadcrumb=page_data.get("add_breadcrumb"),
             prepare_render=page_data.get("prepare_render"),
@@ -313,7 +309,6 @@
             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),
             replace_on_conflict=replace_on_conflict
         )
@@ -321,9 +316,9 @@
     @staticmethod
     def createBrowserData(
         vhost_root,
-        resource: Optional(LiberviaPage),
+        resource: Optional[LiberviaPage],
         browser_path: Path,
-        path_elts: Optional(List[str]),
+        path_elts: Optional[List[str]],
         engine: str = "brython"
     ) -> None:
         """create and store data for browser dynamic code"""
@@ -605,45 +600,6 @@
                 .format( *uri_tuple))
         self.uri_callbacks[uri_tuple] = (self, get_uri_cb)
 
-    def getSignalId(self, request):
-        """Retrieve signal_id for a request
-
-        signal_id is used for dynamic page, to associate a initial request with a
-        signal handler. For WebsocketRequest, signal_id attribute is used (which must
-        be orginal request's id)
-        For server.Request it's id(request)
-        """
-        return getattr(request, 'signal_id', id(request))
-
-    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(_("You can't register signal if page is not dynamic"))
-            return
-        signal_id = self.getSignalId(request)
-        LiberviaPage.signals_handlers.setdefault(signal, {})[signal_id] = [
-            self,
-            request,
-            check_profile,
-        ]
-        request._signals_registered.append(signal)
-
     def getConfig(self, key, default=None, value_type=None):
         return self.host.getConfig(self.vhost_root, key=key, default=default,
                                    value_type=value_type)
@@ -1213,71 +1169,6 @@
 
     # signals, server => browser communication
 
-    @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, {}
-        ).values():
-            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(_("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(
-                    "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
-        # we need to replace corresponding original requests by this websocket request
-        # in signals_handlers
-        signal_id = request.signal_id
-        for signal_handlers_map in self.__class__.signals_handlers.values():
-            if signal_id in signal_handlers_map:
-                signal_handlers_map[signal_id][1] = request
-
-        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:
-            signal_id = self.getSignalId(request)
-            try:
-                del LiberviaPage.signals_handlers[signal][signal_id]
-            except KeyError:
-                log.error(_("Can't find signal handler for [{signal}], this should not "
-                            "happen").format(signal=signal))
-            else:
-                log.debug(_("Removed signal handler"))
-
     def delegateToResource(self, request, resource):
         """continue workflow with Twisted Resource"""
         buf = resource.render(request)
@@ -1447,10 +1338,14 @@
                 raise failure.Failure(exceptions.CancelError("subpage page is used"))
 
     def _prepare_dynamic(self, request):
+        session_data = self.host.getSessionData(request, session_iface.ISATSession)
         # we need to activate dynamic page
         # we set data for template, and create/register token
-        socket_token = str(uuid.uuid4())
-        socket_url = self.host.getWebsocketURL(request)
+        # socket_token = str(uuid.uuid4())
+        socket_url = self.host.get_websocket_url(request)
+        # as for CSRF, it is important to not let the socket token if we use the service
+        # profile, as those pages can be cached, and then the token leaked.
+        socket_token = '' if session_data.profile is None else session_data.ws_token
         socket_debug = C.boolConst(self.host.debug)
         request.template_data["websocket"] = WebsocketMeta(
             socket_url, socket_token, socket_debug
@@ -1459,7 +1354,6 @@
         request._signals_registered = []
         # we will cache registered signals until socket is opened
         request._signals_cache = []
-        self.host.registerWSToken(socket_token, self, request)
 
     def _render_template(self, request):
         template_data = request.template_data
@@ -1759,62 +1653,6 @@
                     session_data.locale = a
                     return
 
-    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(
-                _("renderPartial must only be used with dynamic pages")
-            )
-        session_data = self.host.getSessionData(request, session_iface.ISATSession)
-        if session_data.locale is not None:
-            template_data['locale'] = session_data.locale
-        if self.vhost_root.site_name:
-            template_data['site'] = self.vhost_root.site_name
-
-        return self.host.renderer.render(
-            template,
-            theme=session_data.theme or self.default_theme,
-            site_themes=self.site_themes,
-            page_url=self.getURL(),
-            media_path=f"/{C.MEDIA_DIR}",
-            build_path=f"/{C.BUILD_DIR}/",
-            cache_path=session_data.cache_dir,
-            main_menu=self.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)
-        try:
-            request.sendData(
-                "dom", selectors=selectors, update_type=update_type, html=html)
-        except Exception as e:
-            log.error("Can't renderAndUpdate, html was: {html}".format(html=html))
-            raise e
-
     async 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