Mercurial > libervia-web
comparison 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 |
comparison
equal
deleted
inserted
replaced
961:22fe06569b1a | 962:c7fba7709d05 |
---|---|
1300 class LiberviaPage(web_resource.Resource): | 1300 class LiberviaPage(web_resource.Resource): |
1301 isLeaf = True # we handle subpages ourself | 1301 isLeaf = True # we handle subpages ourself |
1302 named_pages = {} | 1302 named_pages = {} |
1303 uri_callbacks = {} | 1303 uri_callbacks = {} |
1304 | 1304 |
1305 def __init__(self, host, root_dir, name=None, redirect=None, access=None, parse_url=None, | 1305 def __init__(self, host, root_dir, url, name=None, redirect=None, trailing_slash=False, access=None, parse_url=None, |
1306 prepare_render=None, render=None, template=None, on_data_post=None): | 1306 prepare_render=None, render=None, template=None, on_data_post=None): |
1307 """initiate LiberviaPages | 1307 """initiate LiberviaPages |
1308 | 1308 |
1309 LiberviaPages are the main resources of Libervia, using easy to set python files | 1309 LiberviaPages are the main resources of Libervia, using easy to set python files |
1310 The arguments are the variables found in page_meta.py | 1310 The arguments are the variables found in page_meta.py |
1311 @param host(Libervia): the running instance of Libervia | 1311 @param host(Libervia): the running instance of Libervia |
1312 @param root_dir(unicode): aboslute path of the page | 1312 @param root_dir(unicode): aboslute file path of the page |
1313 @param url(unicode): relative URL to the page | |
1314 this URL may not be valid, as pages may require path arguments | |
1313 @param name(unicode, None): if not None, a unique name to identify the page | 1315 @param name(unicode, None): if not None, a unique name to identify the page |
1314 can then be used for e.g. redirection | 1316 can then be used for e.g. redirection |
1315 "/" is not allowed in names (as it can be used to construct URL paths) | 1317 "/" is not allowed in names (as it can be used to construct URL paths) |
1316 @param redirect(unicode, None): if not None, this page will be a redirected | 1318 @param redirect(unicode, None): if not None, this page will be redirected. A redirected |
1317 parameter is used as in self.pageRedirect. parse_url will not be skipped | 1319 parameter is used as in self.pageRedirect. parse_url will not be skipped |
1318 using this redirect parameter is called "full redirection" | 1320 using this redirect parameter is called "full redirection" |
1319 using self.pageRedirect is called "partial redirection" (because some rendering method | 1321 using self.pageRedirect is called "partial redirection" (because some rendering method |
1320 can still be used, e.g. parse_url) | 1322 can still be used, e.g. parse_url) |
1323 @param trailing_slash(bool): if True, page will be redirected to (url + '/') if url is not already ended by a '/'. | |
1324 This is specially useful for relative links | |
1321 @param access(unicode, None): permission needed to access the page | 1325 @param access(unicode, None): permission needed to access the page |
1322 None means public access. | 1326 None means public access. |
1323 Pages inherit from parent pages: e.g. if a "settings" page is restricted to admins, | 1327 Pages inherit from parent pages: e.g. if a "settings" page is restricted to admins, |
1324 and if "settings/blog" is public, it still can only be accessed by admins. | 1328 and if "settings/blog" is public, it still can only be accessed by admins. |
1325 see C.PAGES_ACCESS_* for details | 1329 see C.PAGES_ACCESS_* for details |
1333 This method is mutually exclusive with template and must return a unicode string. | 1337 This method is mutually exclusive with template and must return a unicode string. |
1334 @param template(unicode, None): path to the template to render. | 1338 @param template(unicode, None): path to the template to render. |
1335 This method is mutually exclusive with render | 1339 This method is mutually exclusive with render |
1336 @param on_data_post(callable, None): method to call when data is posted | 1340 @param on_data_post(callable, None): method to call when data is posted |
1337 None if not post is handled | 1341 None if not post is handled |
1342 on_data_post can return a string with following value: | |
1343 - C.POST_NO_CONFIRM: confirm flag will not be set | |
1338 """ | 1344 """ |
1339 | 1345 |
1340 web_resource.Resource.__init__(self) | 1346 web_resource.Resource.__init__(self) |
1341 self.host = host | 1347 self.host = host |
1342 self.root_dir = root_dir | 1348 self.root_dir = root_dir |
1349 self.url = url | |
1343 if name is not None: | 1350 if name is not None: |
1344 if name in self.named_pages: | 1351 if name in self.named_pages: |
1345 raise exceptions.ConflictError(_(u'a Libervia page named "{}" already exists'.format(name))) | 1352 raise exceptions.ConflictError(_(u'a Libervia page named "{}" already exists'.format(name))) |
1346 if u'/' in name: | 1353 if u'/' in name: |
1347 raise ValueError(_(u'"/" is not allowed in page names')) | 1354 raise ValueError(_(u'"/" is not allowed in page names')) |
1360 raise ValueError(_(u"you can't use full page redirection with other rendering method," | 1367 raise ValueError(_(u"you can't use full page redirection with other rendering method," |
1361 u"check self.pageRedirect if you need to use them")) | 1368 u"check self.pageRedirect if you need to use them")) |
1362 self.redirect = redirect | 1369 self.redirect = redirect |
1363 else: | 1370 else: |
1364 self.redirect = None | 1371 self.redirect = None |
1372 self.trailing_slash = trailing_slash | |
1365 self.parse_url = parse_url | 1373 self.parse_url = parse_url |
1366 self.prepare_render = prepare_render | 1374 self.prepare_render = prepare_render |
1367 self.template = template | 1375 self.template = template |
1368 self.render_method = render | 1376 self.render_method = render |
1369 self.on_data_post = on_data_post | 1377 self.on_data_post = on_data_post |
1394 if not os.path.isdir(dir_path): | 1402 if not os.path.isdir(dir_path): |
1395 continue | 1403 continue |
1396 meta_path = os.path.join(dir_path, C.PAGES_META_FILE) | 1404 meta_path = os.path.join(dir_path, C.PAGES_META_FILE) |
1397 if os.path.isfile(meta_path): | 1405 if os.path.isfile(meta_path): |
1398 page_data = {} | 1406 page_data = {} |
1407 new_path = path + [d] | |
1399 # we don't want to force the presence of __init__.py | 1408 # we don't want to force the presence of __init__.py |
1400 # so we use execfile instead of import. | 1409 # so we use execfile instead of import. |
1401 # TODO: when moved to Python 3, __init__.py is not mandatory anymore | 1410 # TODO: when moved to Python 3, __init__.py is not mandatory anymore |
1402 # so we can switch to import | 1411 # so we can switch to import |
1403 execfile(meta_path, page_data) | 1412 execfile(meta_path, page_data) |
1404 resource = LiberviaPage( | 1413 resource = LiberviaPage( |
1405 host, | 1414 host, |
1406 dir_path, | 1415 dir_path, |
1416 u'/' + u'/'.join(new_path), | |
1407 name=page_data.get('name'), | 1417 name=page_data.get('name'), |
1408 redirect=page_data.get('redirect'), | 1418 redirect=page_data.get('redirect'), |
1419 trailing_slash = page_data.get('trailing_slash'), | |
1409 access=page_data.get('access'), | 1420 access=page_data.get('access'), |
1410 parse_url=page_data.get('parse_url'), | 1421 parse_url=page_data.get('parse_url'), |
1411 prepare_render=page_data.get('prepare_render'), | 1422 prepare_render=page_data.get('prepare_render'), |
1412 render=page_data.get('render'), | 1423 render=page_data.get('render'), |
1413 template=page_data.get('template'), | 1424 template=page_data.get('template'), |
1414 on_data_post=page_data.get('on_data_post')) | 1425 on_data_post=page_data.get('on_data_post')) |
1415 parent.putChild(d, resource) | 1426 parent.putChild(d, resource) |
1416 new_path = path + [d] | |
1417 log.info(u"Added /{path} page".format(path=u'[...]/'.join(new_path))) | 1427 log.info(u"Added /{path} page".format(path=u'[...]/'.join(new_path))) |
1418 if 'uri_handlers' in page_data: | 1428 if 'uri_handlers' in page_data: |
1419 if not isinstance(page_data, dict): | 1429 if not isinstance(page_data, dict): |
1420 log.error(_(u'uri_handlers must be a dict')) | 1430 log.error(_(u'uri_handlers must be a dict')) |
1421 else: | 1431 else: |
1473 @return (LiberviaPage): page instance | 1483 @return (LiberviaPage): page instance |
1474 @raise KeyError: the page doesn't exist | 1484 @raise KeyError: the page doesn't exist |
1475 """ | 1485 """ |
1476 return self.named_pages[name] | 1486 return self.named_pages[name] |
1477 | 1487 |
1488 def getPageRedirectURL(self, request, page_name=u'login', url=None): | |
1489 """generate URL for a page with redirect_url parameter set | |
1490 | |
1491 mainly used for login page with redirection to current page | |
1492 @param request(server.Request): current HTTP request | |
1493 @param page_name(unicode): name of the page to go | |
1494 @param url(None, unicode): url to redirect to | |
1495 None to use request path (i.e. current page) | |
1496 @return (unicode): URL to use | |
1497 """ | |
1498 return u'{root_url}?redirect_url={redirect_url}'.format( | |
1499 root_url = self.getPageByName(page_name).url, | |
1500 redirect_url=urllib.quote_plus(request.uri) if url is None else url.encode('utf-8')) | |
1501 | |
1478 def getChildWithDefault(self, path, request): | 1502 def getChildWithDefault(self, path, request): |
1479 # we handle children ourselves | 1503 # we handle children ourselves |
1480 raise exceptions.InternalError(u"this method should not be used with LiberviaPage") | 1504 raise exceptions.InternalError(u"this method should not be used with LiberviaPage") |
1481 | 1505 |
1482 def nextPath(self, request): | 1506 def nextPath(self, request): |
1489 """ | 1513 """ |
1490 pathElement = request.postpath.pop(0) | 1514 pathElement = request.postpath.pop(0) |
1491 request.prepath.append(pathElement) | 1515 request.prepath.append(pathElement) |
1492 return urllib.unquote(pathElement).decode('utf-8') | 1516 return urllib.unquote(pathElement).decode('utf-8') |
1493 | 1517 |
1518 def HTTPRedirect(self, request, url): | |
1519 """redirect to an URL using HTTP redirection | |
1520 | |
1521 @param request(server.Request): current HTTP request | |
1522 @param url(unicode): url to redirect to | |
1523 """ | |
1524 | |
1525 web_util.redirectTo(url.encode('utf-8'), request) | |
1526 request.finish() | |
1527 raise failure.Failure(exceptions.CancelError(u'HTTP redirection is used')) | |
1528 | |
1529 def redirectOrContinue(self, request, redirect_arg=u'redirect_url'): | |
1530 """helper method to redirect a page to an url given as arg | |
1531 | |
1532 if the arg is not present, the page will continue normal workflow | |
1533 @param request(server.Request): current HTTP request | |
1534 @param redirect_arg(unicode): argument to use to get redirection URL | |
1535 @interrupt: redirect the page to requested URL | |
1536 @interrupt pageError(C.HTTP_BAD_REQUEST): empty or non local URL is used | |
1537 """ | |
1538 try: | |
1539 url = self.getPostedData(request, 'redirect_url') | |
1540 except KeyError: | |
1541 pass | |
1542 else: | |
1543 # a redirection is requested | |
1544 if not url or url[0] != u'/': | |
1545 # we only want local urls | |
1546 self.pageError(request, C.HTTP_BAD_REQUEST) | |
1547 else: | |
1548 self.HTTPRedirect(request, url) | |
1549 | |
1494 def pageRedirect(self, page_path, request, skip_parse_url=True): | 1550 def pageRedirect(self, page_path, request, skip_parse_url=True): |
1495 """redirect a page to a named page | 1551 """redirect a page to a named page |
1496 | 1552 |
1497 the workflow will continue with the workflow of the named page, | 1553 the workflow will continue with the workflow of the named page, |
1498 skipping named page's parse_url method if it exist. | 1554 skipping named page's parse_url method if it exist. |
1555 If you want to do a HTTP redirection, use HTTPRedirect | |
1499 @param page_path(unicode): path to page (elements are separated by "/"): | 1556 @param page_path(unicode): path to page (elements are separated by "/"): |
1500 if path starts with a "/": | 1557 if path starts with a "/": |
1501 path is a full path starting from root | 1558 path is a full path starting from root |
1502 else: | 1559 else: |
1503 - first element is name as registered in name variable | 1560 - first element is name as registered in name variable |
1515 redirect_page = self.host.root | 1572 redirect_page = self.host.root |
1516 else: | 1573 else: |
1517 redirect_page = self.named_pages[path[0]] | 1574 redirect_page = self.named_pages[path[0]] |
1518 | 1575 |
1519 for subpage in path[1:]: | 1576 for subpage in path[1:]: |
1520 redirect_page = redirect_page.childen[subpage] | 1577 if redirect_page is self.host.root: |
1578 redirect_page = redirect_page.children[subpage] | |
1579 else: | |
1580 redirect_page = redirect_page.original.children[subpage] | |
1521 | 1581 |
1522 redirect_page.renderPage(request, skip_parse_url=True) | 1582 redirect_page.renderPage(request, skip_parse_url=True) |
1523 raise failure.Failure(exceptions.CancelError(u'page redirection is used')) | 1583 raise failure.Failure(exceptions.CancelError(u'page redirection is used')) |
1524 | 1584 |
1525 def pageError(self, request, code=C.HTTP_NOT_FOUND): | 1585 def pageError(self, request, code=C.HTTP_NOT_FOUND): |
1570 return defer.maybeDeferred(self.prepare_render, self, request) | 1630 return defer.maybeDeferred(self.prepare_render, self, request) |
1571 | 1631 |
1572 def _render_method(self, dummy, request): | 1632 def _render_method(self, dummy, request): |
1573 return defer.maybeDeferred(self.render_method, self, request) | 1633 return defer.maybeDeferred(self.render_method, self, request) |
1574 | 1634 |
1575 def _render_template(self, dummy, template_data): | 1635 def _render_template(self, dummy, request): |
1636 template_data = request.template_data | |
1637 | |
1638 # if confirm variable is set in case of successfuly data post | |
1639 session_data = self.host.getSessionData(request, session_iface.ISATSession) | |
1640 if session_data.popPageFlag(self, C.FLAG_CONFIRM): | |
1641 template_data[u'confirm'] = True | |
1642 | |
1576 return self.host.renderer.render( | 1643 return self.host.renderer.render( |
1577 self.template, | 1644 self.template, |
1578 root_path = '/templates/', | 1645 root_path = '/templates/', |
1646 media_path = '/' + C.MEDIA_DIR, | |
1579 **template_data) | 1647 **template_data) |
1580 | 1648 |
1581 def _renderEb(self, failure_, request): | 1649 def _renderEb(self, failure_, request): |
1582 """don't raise error on CancelError""" | 1650 """don't raise error on CancelError""" |
1583 failure_.trap(exceptions.CancelError) | 1651 failure_.trap(exceptions.CancelError) |
1592 def _on_data_post_redirect(self, ret, request): | 1660 def _on_data_post_redirect(self, ret, request): |
1593 """called when page's on_data_post has been called successfuly | 1661 """called when page's on_data_post has been called successfuly |
1594 | 1662 |
1595 this method redirect to the same page, using Post/Redirect/Get pattern | 1663 this method redirect to the same page, using Post/Redirect/Get pattern |
1596 HTTP status code "See Other" (303) is the recommanded code in this case | 1664 HTTP status code "See Other" (303) is the recommanded code in this case |
1597 """ | 1665 @param ret(None, unicode, iterable): on_data_post return value |
1666 see LiberviaPage.__init__ on_data_post docstring | |
1667 """ | |
1668 if ret is None: | |
1669 ret = () | |
1670 elif isinstance(ret, basestring): | |
1671 ret = (ret,) | |
1672 else: | |
1673 ret = tuple(ret) | |
1674 raise NotImplementedError(_(u'iterable in on_data_post return value is not used yet')) | |
1598 session_data = self.host.getSessionData(request, session_iface.ISATSession) | 1675 session_data = self.host.getSessionData(request, session_iface.ISATSession) |
1599 session_data.flags.add(C.FLAG_CONFIRM) | 1676 if not C.POST_NO_CONFIRM in ret: |
1677 session_data.setPageFlag(self, C.FLAG_CONFIRM) | |
1600 request.setResponseCode(C.HTTP_SEE_OTHER) | 1678 request.setResponseCode(C.HTTP_SEE_OTHER) |
1601 request.setHeader("location", request.uri) | 1679 request.setHeader("location", request.uri) |
1602 request.finish() | 1680 request.finish() |
1603 raise failure.Failure(exceptions.CancelError(u'Post/Redirect/Get is used')) | 1681 raise failure.Failure(exceptions.CancelError(u'Post/Redirect/Get is used')) |
1604 | 1682 |
1615 self.pageError(request, C.HTTP_UNAUTHORIZED) | 1693 self.pageError(request, C.HTTP_UNAUTHORIZED) |
1616 d = defer.maybeDeferred(self.on_data_post, self, request) | 1694 d = defer.maybeDeferred(self.on_data_post, self, request) |
1617 d.addCallback(self._on_data_post_redirect, request) | 1695 d.addCallback(self._on_data_post_redirect, request) |
1618 return d | 1696 return d |
1619 | 1697 |
1620 | |
1621 def getPostedData(self, request, keys, multiple=False): | 1698 def getPostedData(self, request, keys, multiple=False): |
1622 """get data from a POST request and decode it | 1699 """get data from a POST request and decode it |
1623 | 1700 |
1624 @param request(server.Request): request linked to the session | 1701 @param request(server.Request): request linked to the session |
1625 @param keys(unicode, iterable[unicode]): name of the value(s) to get | 1702 @param keys(unicode, iterable[unicode]): name of the value(s) to get |
1643 ret.append(gen) | 1720 ret.append(gen) |
1644 else: | 1721 else: |
1645 try: | 1722 try: |
1646 ret.append(next(gen)) | 1723 ret.append(next(gen)) |
1647 except StopIteration: | 1724 except StopIteration: |
1648 return KeyError(key) | 1725 raise KeyError(key) |
1649 | 1726 |
1650 return ret[0] if get_first else ret | 1727 return ret[0] if get_first else ret |
1651 | 1728 |
1652 def getAllPostedData(self, request, except_=()): | 1729 def getAllPostedData(self, request, except_=()): |
1653 """get all posted data | 1730 """get all posted data |
1697 if self.access == C.PAGES_ACCESS_PUBLIC: | 1774 if self.access == C.PAGES_ACCESS_PUBLIC: |
1698 pass | 1775 pass |
1699 elif self.access == C.PAGES_ACCESS_PROFILE: | 1776 elif self.access == C.PAGES_ACCESS_PROFILE: |
1700 profile = self.getProfile(request) | 1777 profile = self.getProfile(request) |
1701 if not profile: | 1778 if not profile: |
1702 # no session started, access is not granted | 1779 # no session started |
1703 self.pageError(request, C.HTTP_UNAUTHORIZED) | 1780 if not self.host.options["allow_registration"]: |
1781 # registration not allowed, access is not granted | |
1782 self.pageError(request, C.HTTP_UNAUTHORIZED) | |
1783 else: | |
1784 # registration allowed, we redirect to login page | |
1785 login_url = self.getPageRedirectURL(request) | |
1786 self.HTTPRedirect(request, login_url) | |
1704 | 1787 |
1705 return data | 1788 return data |
1706 | 1789 |
1707 def renderPage(self, request, skip_parse_url=False): | 1790 def renderPage(self, request, skip_parse_url=False): |
1708 """Main method to handle the workflow of a LiberviaPage""" | 1791 """Main method to handle the workflow of a LiberviaPage""" |
1709 # template_data are the variables passed to template | 1792 # template_data are the variables passed to template |
1710 if not hasattr(request, 'template_data'): | 1793 if not hasattr(request, 'template_data'): |
1794 if self.trailing_slash and request.path and not request.path[-1] == '/': | |
1795 return web_util.redirectTo(request.path + '/', request) | |
1711 session_data = self.host.getSessionData(request, session_iface.ISATSession) | 1796 session_data = self.host.getSessionData(request, session_iface.ISATSession) |
1712 csrf_token = session_data.csrf_token | 1797 csrf_token = session_data.csrf_token |
1713 request.template_data = {u'csrf_token': csrf_token} | 1798 request.template_data = {u'csrf_token': csrf_token} |
1714 if C.FLAG_CONFIRM in session_data.flags: | |
1715 request.template_data[u'confirm'] = True | |
1716 session_data.flags.remove(C.FLAG_CONFIRM) | |
1717 | 1799 |
1718 # XXX: here is the code which need to be executed once | 1800 # XXX: here is the code which need to be executed once |
1719 # at the beginning of the request hanling | 1801 # at the beginning of the request hanling |
1720 if request.postpath and not request.postpath[-1]: | 1802 if request.postpath and not request.postpath[-1]: |
1721 # we don't differenciate URLs finishing with '/' or not | 1803 # we don't differenciate URLs finishing with '/' or not |
1733 d.addCallback(self._subpagesHandler, request) | 1815 d.addCallback(self._subpagesHandler, request) |
1734 | 1816 |
1735 if request.method not in (C.HTTP_METHOD_GET, C.HTTP_METHOD_POST): | 1817 if request.method not in (C.HTTP_METHOD_GET, C.HTTP_METHOD_POST): |
1736 # only HTTP GET and POST are handled so far | 1818 # only HTTP GET and POST are handled so far |
1737 d.addCallback(lambda dummy: self.pageError(request, C.HTTP_BAD_REQUEST)) | 1819 d.addCallback(lambda dummy: self.pageError(request, C.HTTP_BAD_REQUEST)) |
1738 | |
1739 | 1820 |
1740 if request.method == C.HTTP_METHOD_POST: | 1821 if request.method == C.HTTP_METHOD_POST: |
1741 if self.on_data_post is None: | 1822 if self.on_data_post is None: |
1742 # if we don't have on_data_post, the page was not expecting POST | 1823 # if we don't have on_data_post, the page was not expecting POST |
1743 # so we return an error | 1824 # so we return an error |
1749 | 1830 |
1750 if self.prepare_render: | 1831 if self.prepare_render: |
1751 d.addCallback(self._prepare_render, request) | 1832 d.addCallback(self._prepare_render, request) |
1752 | 1833 |
1753 if self.template: | 1834 if self.template: |
1754 d.addCallback(self._render_template, request.template_data) | 1835 d.addCallback(self._render_template, request) |
1755 elif self.render_method: | 1836 elif self.render_method: |
1756 d.addCallback(self._render_method, request) | 1837 d.addCallback(self._render_method, request) |
1757 | 1838 |
1758 d.addCallback(self.writeData, request) | 1839 d.addCallback(self.writeData, request) |
1759 d.addErrback(self._renderEb, request) | 1840 d.addErrback(self._renderEb, request) |