diff src/server/server.py @ 962:c7fba7709d05

Pages: various improvments: - automatic confirmation message on data post can now be avoided by using the C.POST_NO_CONFIRM flag - new tailing_slash page variable can be used to force a trailing slash at the end of the URL (by redirecting if necessary) - LiberviaPage now has a url attribute with the its relative path - new redirection methods: - getPageRedirectURL: generate and URL which will redirect to current page (or somewhere else), mainly useful for login - HTTPRedirect: stop workflow and do a HTTP redirection - redirectOrContinue: redirect a page if redirect arguments is present (usually redirect_url), else continue workflow - profile access now redirect to login page if registration is allowed.
author Goffi <goffi@goffi.org>
date Fri, 27 Oct 2017 18:43:16 +0200
parents 22fe06569b1a
children fd4eae654182
line wrap: on
line diff
--- a/src/server/server.py	Fri Oct 27 18:35:23 2017 +0200
+++ b/src/server/server.py	Fri Oct 27 18:43:16 2017 +0200
@@ -1302,22 +1302,26 @@
     named_pages = {}
     uri_callbacks = {}
 
-    def __init__(self, host, root_dir, name=None, redirect=None, access=None, parse_url=None,
+    def __init__(self, host, root_dir, url, name=None, redirect=None, trailing_slash=False, access=None, parse_url=None,
                  prepare_render=None, render=None, template=None, on_data_post=None):
         """initiate LiberviaPages
 
         LiberviaPages are the main resources of Libervia, using easy to set python files
         The arguments are the variables found in page_meta.py
         @param host(Libervia): the running instance of Libervia
-        @param root_dir(unicode): aboslute path of the page
+        @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 a redirected
+        @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 trailing_slash(bool): if True, page will be redirected to (url + '/') if url is not already ended by a '/'.
+            This is specially useful for relative links
         @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,
@@ -1335,11 +1339,14 @@
             This method is mutually exclusive with render
         @param on_data_post(callable, None): method to call when data is posted
             None if not post is handled
+            on_data_post can return a string with following value:
+                - C.POST_NO_CONFIRM: confirm flag will not be set
         """
 
         web_resource.Resource.__init__(self)
         self.host = host
         self.root_dir = root_dir
+        self.url = url
         if name is not None:
             if name in self.named_pages:
                 raise exceptions.ConflictError(_(u'a Libervia page named "{}" already exists'.format(name)))
@@ -1362,6 +1369,7 @@
             self.redirect = redirect
         else:
             self.redirect = None
+        self.trailing_slash = trailing_slash
         self.parse_url = parse_url
         self.prepare_render = prepare_render
         self.template = template
@@ -1396,6 +1404,7 @@
             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
@@ -1404,8 +1413,10 @@
                 resource = LiberviaPage(
                     host,
                     dir_path,
+                    u'/' + u'/'.join(new_path),
                     name=page_data.get('name'),
                     redirect=page_data.get('redirect'),
+                    trailing_slash = page_data.get('trailing_slash'),
                     access=page_data.get('access'),
                     parse_url=page_data.get('parse_url'),
                     prepare_render=page_data.get('prepare_render'),
@@ -1413,7 +1424,6 @@
                     template=page_data.get('template'),
                     on_data_post=page_data.get('on_data_post'))
                 parent.putChild(d, resource)
-                new_path = path + [d]
                 log.info(u"Added /{path} page".format(path=u'[...]/'.join(new_path)))
                 if 'uri_handlers' in page_data:
                     if not isinstance(page_data, dict):
@@ -1475,6 +1485,20 @@
         """
         return self.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 getChildWithDefault(self, path, request):
         # we handle children ourselves
         raise exceptions.InternalError(u"this method should not be used with LiberviaPage")
@@ -1491,11 +1515,44 @@
         request.prepath.append(pathElement)
         return urllib.unquote(pathElement).decode('utf-8')
 
+    def HTTPRedirect(self, request, url):
+        """redirect to an URL using HTTP redirection
+
+        @param request(server.Request): current HTTP request
+        @param url(unicode): url to redirect to
+        """
+
+        web_util.redirectTo(url.encode('utf-8'), request)
+        request.finish()
+        raise failure.Failure(exceptions.CancelError(u'HTTP redirection is used'))
+
+    def redirectOrContinue(self, request, redirect_arg=u'redirect_url'):
+        """helper method to redirect a page to an url given as arg
+
+        if the arg is not present, the page will continue normal workflow
+        @param request(server.Request): current HTTP request
+        @param redirect_arg(unicode): argument to use to get redirection URL
+        @interrupt: redirect the page to requested URL
+        @interrupt pageError(C.HTTP_BAD_REQUEST): empty or non local URL is used
+        """
+        try:
+            url = self.getPostedData(request, 'redirect_url')
+        except KeyError:
+            pass
+        else:
+            # a redirection is requested
+            if not url or url[0] != u'/':
+                # we only want local urls
+                self.pageError(request, C.HTTP_BAD_REQUEST)
+            else:
+                self.HTTPRedirect(request, url)
+
     def pageRedirect(self, page_path, request, skip_parse_url=True):
         """redirect a page to a named page
 
         the workflow will continue with the workflow of the named page,
         skipping named page's parse_url method if it exist.
+        If you want to do a HTTP redirection, use HTTPRedirect
         @param page_path(unicode): path to page (elements are separated by "/"):
             if path starts with a "/":
                 path is a full path starting from root
@@ -1517,7 +1574,10 @@
             redirect_page = self.named_pages[path[0]]
 
         for subpage in path[1:]:
-            redirect_page = redirect_page.childen[subpage]
+            if redirect_page is self.host.root:
+                redirect_page = redirect_page.children[subpage]
+            else:
+                redirect_page = redirect_page.original.children[subpage]
 
         redirect_page.renderPage(request, skip_parse_url=True)
         raise failure.Failure(exceptions.CancelError(u'page redirection is used'))
@@ -1572,10 +1632,18 @@
     def _render_method(self, dummy, request):
         return defer.maybeDeferred(self.render_method, self, request)
 
-    def _render_template(self, dummy, template_data):
+    def _render_template(self, dummy, request):
+        template_data = request.template_data
+
+        # if confirm variable is set in case of successfuly data post
+        session_data = self.host.getSessionData(request, session_iface.ISATSession)
+        if session_data.popPageFlag(self, C.FLAG_CONFIRM):
+            template_data[u'confirm'] = True
+
         return self.host.renderer.render(
             self.template,
             root_path = '/templates/',
+            media_path = '/' + C.MEDIA_DIR,
             **template_data)
 
     def _renderEb(self, failure_, request):
@@ -1594,9 +1662,19 @@
 
         this method redirect to the same page, using Post/Redirect/Get pattern
         HTTP status code "See Other" (303) 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)
-        session_data.flags.add(C.FLAG_CONFIRM)
+        if not C.POST_NO_CONFIRM in ret:
+            session_data.setPageFlag(self, C.FLAG_CONFIRM)
         request.setResponseCode(C.HTTP_SEE_OTHER)
         request.setHeader("location", request.uri)
         request.finish()
@@ -1617,7 +1695,6 @@
         d.addCallback(self._on_data_post_redirect, request)
         return d
 
-
     def getPostedData(self, request, keys, multiple=False):
         """get data from a POST request and decode it
 
@@ -1645,7 +1722,7 @@
                 try:
                     ret.append(next(gen))
                 except StopIteration:
-                    return KeyError(key)
+                    raise KeyError(key)
 
         return ret[0] if get_first else ret
 
@@ -1699,8 +1776,14 @@
         elif self.access == C.PAGES_ACCESS_PROFILE:
             profile = self.getProfile(request)
             if not profile:
-                # no session started, access is not granted
-                self.pageError(request, C.HTTP_UNAUTHORIZED)
+                # 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
 
@@ -1708,12 +1791,11 @@
         """Main method to handle the workflow of a LiberviaPage"""
         # template_data are the variables passed to template
         if not hasattr(request, 'template_data'):
+            if self.trailing_slash and request.path and not request.path[-1] == '/':
+                return web_util.redirectTo(request.path + '/', request)
             session_data = self.host.getSessionData(request, session_iface.ISATSession)
             csrf_token = session_data.csrf_token
             request.template_data = {u'csrf_token': csrf_token}
-            if C.FLAG_CONFIRM in session_data.flags:
-                request.template_data[u'confirm'] = True
-                session_data.flags.remove(C.FLAG_CONFIRM)
 
             # XXX: here is the code which need to be executed once
             #      at the beginning of the request hanling
@@ -1736,7 +1818,6 @@
             # 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
@@ -1751,7 +1832,7 @@
             d.addCallback(self._prepare_render, request)
 
         if self.template:
-            d.addCallback(self._render_template, request.template_data)
+            d.addCallback(self._render_template, request)
         elif self.render_method:
             d.addCallback(self._render_method, request)