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)