# HG changeset patch # User Goffi # Date 1509122596 -7200 # Node ID c7fba7709d0504c30b426344393cbf0c4f079df1 # Parent 22fe06569b1a6baeacd916baec0b18648ad91166 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. diff -r 22fe06569b1a -r c7fba7709d05 src/server/constants.py --- a/src/server/constants.py Fri Oct 27 18:35:23 2017 +0200 +++ b/src/server/constants.py Fri Oct 27 18:43:16 2017 +0200 @@ -59,7 +59,10 @@ PAGES_ACCESS_ALL = (PAGES_ACCESS_NONE, PAGES_ACCESS_PUBLIC, PAGES_ACCESS_PROFILE, PAGES_ACCESS_ADMIN) ## Session flags ## - FLAG_CONFIRM = u"confirm" + FLAG_CONFIRM = u"CONFIRM" + + ## Data post ## + POST_NO_CONFIRM = u"POST_NO_CONFIRM" ## HTTP methods ## HTTP_METHOD_GET = u'GET' diff -r 22fe06569b1a -r c7fba7709d05 src/server/server.py --- 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)