Mercurial > libervia-web
comparison libervia/server/pages.py @ 1276:cad8f24e23d4
pages: use a coroutine for renderPage:
renderPage is now a coroutine, and pages methods are launched using asDeferred, so they
can be coroutines too.
Full traceback is now logged for uncatched errors.
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 19 Jun 2020 16:47:50 +0200 |
parents | 334d044f2713 |
children | 2e4fcd31f2a9 |
comparison
equal
deleted
inserted
replaced
1275:334d044f2713 | 1276:cad8f24e23d4 |
---|---|
23 import urllib.request, urllib.parse, urllib.error | 23 import urllib.request, urllib.parse, urllib.error |
24 import time | 24 import time |
25 import hashlib | 25 import hashlib |
26 import copy | 26 import copy |
27 import json | 27 import json |
28 import traceback | |
28 from pathlib import Path | 29 from pathlib import Path |
29 from functools import reduce | 30 from functools import reduce |
30 from typing import Optional, List | 31 from typing import Optional, List |
31 | 32 |
32 from twisted.web import server | 33 from twisted.web import server |
36 from twisted.words.protocols.jabber import jid | 37 from twisted.words.protocols.jabber import jid |
37 from twisted.python import failure | 38 from twisted.python import failure |
38 | 39 |
39 from sat.core.i18n import _ | 40 from sat.core.i18n import _ |
40 from sat.core import exceptions | 41 from sat.core import exceptions |
42 from sat.tools.utils import asDeferred | |
41 from sat.tools.common import date_utils | 43 from sat.tools.common import date_utils |
42 from sat.tools.common import utils | 44 from sat.tools.common import utils |
43 from sat.core.log import getLogger | 45 from sat.core.log import getLogger |
44 from sat_frontends.bridge.bridge_frontend import BridgeException | 46 from sat_frontends.bridge.bridge_frontend import BridgeException |
45 | 47 |
226 if access == C.PAGES_ACCESS_NONE: | 228 if access == C.PAGES_ACCESS_NONE: |
227 # none pages just return a 404, no further check is needed | 229 # none pages just return a 404, no further check is needed |
228 return | 230 return |
229 if template is not None and render is not None: | 231 if template is not None and render is not None: |
230 log.error(_("render and template methods can't be used at the same time")) | 232 log.error(_("render and template methods can't be used at the same time")) |
231 if parse_url is not None and not callable(parse_url): | |
232 log.error(_("parse_url must be a callable")) | |
233 | 233 |
234 # if not None, next rendering will be cached | 234 # if not None, next rendering will be cached |
235 # it must then contain a list of the the keys to use (without the page instance) | 235 # it must then contain a list of the the keys to use (without the page instance) |
236 # e.g. [C.SERVICE_PROFILE, "pubsub", server@example.tld, pubsub_node] | 236 # e.g. [C.SERVICE_PROFILE, "pubsub", server@example.tld, pubsub_node] |
237 self._do_cache = None | 237 self._do_cache = None |
1041 self._checkCacheHeaders(request, cache) | 1041 self._checkCacheHeaders(request, cache) |
1042 request.write(cache.rendered) | 1042 request.write(cache.rendered) |
1043 request.finish() | 1043 request.finish() |
1044 raise failure.Failure(exceptions.CancelError("cache is used")) | 1044 raise failure.Failure(exceptions.CancelError("cache is used")) |
1045 | 1045 |
1046 def _cacheURL(self, __, request, profile): | 1046 def _cacheURL(self, request, profile): |
1047 self.cached_urls.setdefault(profile, {})[request.uri] = CacheURL(request) | 1047 self.cached_urls.setdefault(profile, {})[request.uri] = CacheURL(request) |
1048 | 1048 |
1049 @classmethod | 1049 @classmethod |
1050 def onNodeEvent(cls, host, service, node, event_type, items, profile): | 1050 def onNodeEvent(cls, host, service, node, event_type, items, profile): |
1051 """Invalidate cache for all pages linked to this node""" | 1051 """Invalidate cache for all pages linked to this node""" |
1218 if self._do_cache: | 1218 if self._do_cache: |
1219 # if cache is needed, it will be handled by final page | 1219 # if cache is needed, it will be handled by final page |
1220 redirect_page._do_cache = self._do_cache | 1220 redirect_page._do_cache = self._do_cache |
1221 self._do_cache = None | 1221 self._do_cache = None |
1222 | 1222 |
1223 redirect_page.renderPage(request, skip_parse_url=skip_parse_url) | 1223 defer.ensureDeferred( |
1224 redirect_page.renderPage(request, skip_parse_url=skip_parse_url) | |
1225 ) | |
1224 raise failure.Failure(exceptions.CancelError("page redirection is used")) | 1226 raise failure.Failure(exceptions.CancelError("page redirection is used")) |
1225 | 1227 |
1226 def pageError(self, request, code=C.HTTP_NOT_FOUND, no_body=False): | 1228 def pageError(self, request, code=C.HTTP_NOT_FOUND, no_body=False): |
1227 """generate an error page and terminate the request | 1229 """generate an error page and terminate the request |
1228 | 1230 |
1278 log.warning(_("Can't write page, the request has probably been cancelled " | 1280 log.warning(_("Can't write page, the request has probably been cancelled " |
1279 "(browser tab closed or reloaded)")) | 1281 "(browser tab closed or reloaded)")) |
1280 return | 1282 return |
1281 request.finish() | 1283 request.finish() |
1282 | 1284 |
1283 def _subpagesHandler(self, __, request): | 1285 def _subpagesHandler(self, request): |
1284 """render subpage if suitable | 1286 """render subpage if suitable |
1285 | 1287 |
1286 this method checks if there is still an unmanaged part of the path | 1288 this method checks if there is still an unmanaged part of the path |
1287 and check if it corresponds to a subpage. If so, it render the subpage | 1289 and check if it corresponds to a subpage. If so, it render the subpage |
1288 else it render a NoResource. | 1290 else it render a NoResource. |
1296 self.pageError(request) | 1298 self.pageError(request) |
1297 else: | 1299 else: |
1298 child.render(request) | 1300 child.render(request) |
1299 raise failure.Failure(exceptions.CancelError("subpage page is used")) | 1301 raise failure.Failure(exceptions.CancelError("subpage page is used")) |
1300 | 1302 |
1301 def _prepare_dynamic(self, __, request): | 1303 def _prepare_dynamic(self, request): |
1302 # we need to activate dynamic page | 1304 # we need to activate dynamic page |
1303 # we set data for template, and create/register token | 1305 # we set data for template, and create/register token |
1304 socket_token = str(uuid.uuid4()) | 1306 socket_token = str(uuid.uuid4()) |
1305 socket_url = self.host.getWebsocketURL(request) | 1307 socket_url = self.host.getWebsocketURL(request) |
1306 socket_debug = C.boolConst(self.host.debug) | 1308 socket_debug = C.boolConst(self.host.debug) |
1311 request._signals_registered = [] | 1313 request._signals_registered = [] |
1312 # we will cache registered signals until socket is opened | 1314 # we will cache registered signals until socket is opened |
1313 request._signals_cache = [] | 1315 request._signals_cache = [] |
1314 self.host.registerWSToken(socket_token, self, request) | 1316 self.host.registerWSToken(socket_token, self, request) |
1315 | 1317 |
1316 def _prepare_render(self, __, request): | 1318 def _render_template(self, request): |
1317 return defer.maybeDeferred(self.prepare_render, self, request) | |
1318 | |
1319 def _render_method(self, __, request): | |
1320 return defer.maybeDeferred(self.render_method, self, request) | |
1321 | |
1322 def _render_template(self, __, request): | |
1323 template_data = request.template_data | 1319 template_data = request.template_data |
1324 | 1320 |
1325 # if confirm variable is set in case of successfuly data post | 1321 # if confirm variable is set in case of successfuly data post |
1326 session_data = self.host.getSessionData(request, session_iface.ISATSession) | 1322 session_data = self.host.getSessionData(request, session_iface.ISATSession) |
1327 template_data['identities'] = session_data.identities | 1323 template_data['identities'] = session_data.identities |
1363 cache_path=session_data.cache_dir, | 1359 cache_path=session_data.cache_dir, |
1364 build_path=f"/{C.BUILD_DIR}/", | 1360 build_path=f"/{C.BUILD_DIR}/", |
1365 main_menu=self.main_menu, | 1361 main_menu=self.main_menu, |
1366 **template_data) | 1362 **template_data) |
1367 | 1363 |
1368 def _renderEb(self, failure_, request): | |
1369 """don't raise error on CancelError""" | |
1370 failure_.trap(exceptions.CancelError) | |
1371 | |
1372 def _internalError(self, failure_, request): | |
1373 """called if an error is not catched""" | |
1374 if failure_.check(BridgeException) and failure_.value.condition == 'not-allowed': | |
1375 log.warning("not allowed exception catched") | |
1376 self.pageError(request, C.HTTP_FORBIDDEN) | |
1377 log.error(_("Uncatched error for HTTP request on {url}: {msg}") | |
1378 .format(url=request.URLPath(), msg=failure_)) | |
1379 self.pageError(request, C.HTTP_INTERNAL_ERROR) | |
1380 | |
1381 def _on_data_post_error(self, failure_, request): | |
1382 failure_.trap(exceptions.DataError) | |
1383 # something is wrong with the posted data, we re-display the page with a | |
1384 # warning notification | |
1385 session_data = self.host.getSessionData(request, session_iface.ISATSession) | |
1386 session_data.setPageNotification(self, failure_.value.message, C.LVL_WARNING) | |
1387 request.setResponseCode(C.HTTP_SEE_OTHER) | |
1388 request.setHeader("location", request.uri) | |
1389 request.finish() | |
1390 raise failure.Failure(exceptions.CancelError("Post/Redirect/Get is used")) | |
1391 | |
1392 def _on_data_post_redirect(self, ret, request): | 1364 def _on_data_post_redirect(self, ret, request): |
1393 """called when page's on_data_post has been done successfuly | 1365 """called when page's on_data_post has been done successfuly |
1394 | 1366 |
1395 This will do a Post/Redirect/Get pattern. | 1367 This will do a Post/Redirect/Get pattern. |
1396 this method redirect to the same page or to request.data['post_redirect_page'] | 1368 this method redirect to the same page or to request.data['post_redirect_page'] |
1432 request.setResponseCode(C.HTTP_SEE_OTHER) | 1404 request.setResponseCode(C.HTTP_SEE_OTHER) |
1433 request.setHeader(b"location", redirect_uri) | 1405 request.setHeader(b"location", redirect_uri) |
1434 request.finish() | 1406 request.finish() |
1435 raise failure.Failure(exceptions.CancelError("Post/Redirect/Get is used")) | 1407 raise failure.Failure(exceptions.CancelError("Post/Redirect/Get is used")) |
1436 | 1408 |
1437 def _on_data_post(self, __, request): | 1409 async def _on_data_post(self, request): |
1438 csrf_token = self.host.getSessionData( | 1410 csrf_token = self.host.getSessionData( |
1439 request, session_iface.ISATSession | 1411 request, session_iface.ISATSession |
1440 ).csrf_token | 1412 ).csrf_token |
1441 try: | 1413 try: |
1442 given_csrf = self.getPostedData(request, "csrf_token") | 1414 given_csrf = self.getPostedData(request, "csrf_token") |
1447 _("invalid CSRF token, hack attempt? URL: {url}, IP: {ip}").format( | 1419 _("invalid CSRF token, hack attempt? URL: {url}, IP: {ip}").format( |
1448 url=request.uri, ip=request.getClientIP() | 1420 url=request.uri, ip=request.getClientIP() |
1449 ) | 1421 ) |
1450 ) | 1422 ) |
1451 self.pageError(request, C.HTTP_FORBIDDEN) | 1423 self.pageError(request, C.HTTP_FORBIDDEN) |
1452 d = defer.maybeDeferred(self.on_data_post, self, request) | 1424 try: |
1453 d.addCallback(self._on_data_post_redirect, request) | 1425 ret = await asDeferred(self.on_data_post, self, request) |
1454 d.addErrback(self._on_data_post_error, request) | 1426 except exceptions.DataError as e: |
1455 return d | 1427 # something is wrong with the posted data, we re-display the page with a |
1428 # warning notification | |
1429 session_data = self.host.getSessionData(request, session_iface.ISATSession) | |
1430 session_data.setPageNotification(self, e.value.message, C.LVL_WARNING) | |
1431 request.setResponseCode(C.HTTP_SEE_OTHER) | |
1432 request.setHeader("location", request.uri) | |
1433 request.finish() | |
1434 raise failure.Failure(exceptions.CancelError("Post/Redirect/Get is used")) | |
1435 else: | |
1436 self._on_data_post_redirect(ret, request) | |
1456 | 1437 |
1457 def getPostedData(self, request, keys, multiple=False, raise_on_missing=True): | 1438 def getPostedData(self, request, keys, multiple=False, raise_on_missing=True): |
1458 """Get data from a POST request or from URL's query part and decode it | 1439 """Get data from a POST request or from URL's query part and decode it |
1459 | 1440 |
1460 @param request(server.Request): request linked to the session | 1441 @param request(server.Request): request linked to the session |
1540 return request.data | 1521 return request.data |
1541 except AttributeError: | 1522 except AttributeError: |
1542 request.data = {} | 1523 request.data = {} |
1543 return request.data | 1524 return request.data |
1544 | 1525 |
1545 def _checkAccess(self, data, request): | 1526 def _checkAccess(self, request): |
1546 """Check access according to self.access | 1527 """Check access according to self.access |
1547 | 1528 |
1548 if access is not granted, show a HTTP_FORBIDDEN pageError and stop request, | 1529 if access is not granted, show a HTTP_FORBIDDEN pageError and stop request, |
1549 else return data (so it can be inserted in deferred chain | 1530 else return data (so it can be inserted in deferred chain |
1550 """ | 1531 """ |
1559 self.pageError(request, C.HTTP_FORBIDDEN) | 1540 self.pageError(request, C.HTTP_FORBIDDEN) |
1560 else: | 1541 else: |
1561 # registration allowed, we redirect to login page | 1542 # registration allowed, we redirect to login page |
1562 login_url = self.getPageRedirectURL(request) | 1543 login_url = self.getPageRedirectURL(request) |
1563 self.HTTPRedirect(request, login_url) | 1544 self.HTTPRedirect(request, login_url) |
1564 | |
1565 return data | |
1566 | 1545 |
1567 def setBestLocale(self, request): | 1546 def setBestLocale(self, request): |
1568 """Guess the best locale when it is not specified explicitly by user | 1547 """Guess the best locale when it is not specified explicitly by user |
1569 | 1548 |
1570 This method will check "accept-language" header, and set locale to first | 1549 This method will check "accept-language" header, and set locale to first |
1640 "dom", selectors=selectors, update_type=update_type, html=html) | 1619 "dom", selectors=selectors, update_type=update_type, html=html) |
1641 except Exception as e: | 1620 except Exception as e: |
1642 log.error("Can't renderAndUpdate, html was: {html}".format(html=html)) | 1621 log.error("Can't renderAndUpdate, html was: {html}".format(html=html)) |
1643 raise e | 1622 raise e |
1644 | 1623 |
1645 def renderPage(self, request, skip_parse_url=False): | 1624 async def renderPage(self, request, skip_parse_url=False): |
1646 """Main method to handle the workflow of a LiberviaPage""" | 1625 """Main method to handle the workflow of a LiberviaPage""" |
1647 | 1626 |
1648 # template_data are the variables passed to template | 1627 # template_data are the variables passed to template |
1649 if not hasattr(request, "template_data"): | 1628 if not hasattr(request, "template_data"): |
1650 # if template_data doesn't exist, it's the beginning of the request workflow | 1629 # if template_data doesn't exist, it's the beginning of the request workflow |
1693 .format(theme=theme, vhost=self.vhost_root))) | 1672 .format(theme=theme, vhost=self.vhost_root))) |
1694 else: | 1673 else: |
1695 session_data.theme = theme | 1674 session_data.theme = theme |
1696 | 1675 |
1697 | 1676 |
1698 d = defer.Deferred() | 1677 try: |
1699 d.addCallback(self._checkAccess, request) | 1678 |
1700 | 1679 try: |
1701 if self.redirect is not None: | 1680 self._checkAccess(request) |
1702 d.addCallback( | 1681 |
1703 lambda __: self.pageRedirect( | 1682 if self.redirect is not None: |
1704 self.redirect, request, skip_parse_url=False | 1683 self.pageRedirect(self.redirect, request, skip_parse_url=False) |
1684 | |
1685 if self.parse_url is not None and not skip_parse_url: | |
1686 if self.url_cache: | |
1687 profile = self.getProfile(request) | |
1688 try: | |
1689 cache_url = self.cached_urls[profile][request.uri] | |
1690 except KeyError: | |
1691 # no cache for this URI yet | |
1692 # we do normal URL parsing, and then the cache | |
1693 await asDeferred(self.parse_url, self, request) | |
1694 self._cacheURL(request, profile) | |
1695 else: | |
1696 log.debug(f"using URI cache for {self}") | |
1697 cache_url.use(request) | |
1698 else: | |
1699 await asDeferred(self.parse_url, self, request) | |
1700 | |
1701 self._subpagesHandler(request) | |
1702 | |
1703 if request.method not in (C.HTTP_METHOD_GET, C.HTTP_METHOD_POST): | |
1704 # only HTTP GET and POST are handled so far | |
1705 self.pageError(request, C.HTTP_BAD_REQUEST) | |
1706 | |
1707 if request.method == C.HTTP_METHOD_POST: | |
1708 if self.on_data_post is None: | |
1709 # if we don't have on_data_post, the page was not expecting POST | |
1710 # so we return an error | |
1711 self.pageError(request, C.HTTP_BAD_REQUEST) | |
1712 else: | |
1713 await self._on_data_post(request) | |
1714 # by default, POST follow normal behaviour after on_data_post is called | |
1715 # this can be changed by a redirection or other method call in on_data_post | |
1716 | |
1717 if self.dynamic: | |
1718 self._prepare_dynamic(request) | |
1719 | |
1720 if self.prepare_render: | |
1721 await asDeferred(self.prepare_render, self, request) | |
1722 | |
1723 if self.template: | |
1724 rendered = self._render_template(request) | |
1725 elif self.render_method: | |
1726 rendered = await asDeferred(self.render_method, self, request) | |
1727 else: | |
1728 raise exceptions.InternalError( | |
1729 "No method set to render page, please set a template or use a " | |
1730 "render method" | |
1731 ) | |
1732 | |
1733 self.writeData(rendered, request) | |
1734 | |
1735 except failure.Failure as f: | |
1736 # we have to unpack to Failure to catch the right Exception | |
1737 raise f.value | |
1738 | |
1739 except exceptions.CancelError: | |
1740 pass | |
1741 except BridgeException as e: | |
1742 if e.condition == 'not-allowed': | |
1743 log.warning("not allowed exception catched") | |
1744 self.pageError(request, C.HTTP_FORBIDDEN) | |
1745 else: | |
1746 log.error(_("Uncatched bridge exception for HTTP request on {url}: {e}") | |
1747 .format(url=request.URLPath(), e=e)) | |
1748 try: | |
1749 self.pageError(request, C.HTTP_INTERNAL_ERROR) | |
1750 except exceptions.CancelError: | |
1751 pass | |
1752 except Exception as e: | |
1753 tb = traceback.format_exc() | |
1754 log.error( | |
1755 _("Uncatched error for HTTP request on {url}:\n{tb}") | |
1756 .format( | |
1757 url=request.URLPath(), | |
1758 e_name=e.__class__.__name__, | |
1759 e=e, | |
1760 tb=tb, | |
1705 ) | 1761 ) |
1706 ) | 1762 ) |
1707 | 1763 try: |
1708 if self.parse_url is not None and not skip_parse_url: | 1764 self.pageError(request, C.HTTP_INTERNAL_ERROR) |
1709 if self.url_cache: | 1765 except exceptions.CancelError: |
1710 profile = self.getProfile(request) | 1766 pass |
1711 try: | 1767 |
1712 cache_url = self.cached_urls[profile][request.uri] | 1768 def render_GET(self, request): |
1713 except KeyError: | 1769 defer.ensureDeferred(self.renderPage(request)) |
1714 # no cache for this URI yet | |
1715 # we do normal URL parsing, and then the cache | |
1716 d.addCallback(self.parse_url, request) | |
1717 d.addCallback(self._cacheURL, request, profile) | |
1718 else: | |
1719 log.debug(_("using URI cache for {page}").format(page=self)) | |
1720 cache_url.use(request) | |
1721 else: | |
1722 d.addCallback(self.parse_url, request) | |
1723 | |
1724 d.addCallback(self._subpagesHandler, request) | |
1725 | |
1726 if request.method not in (C.HTTP_METHOD_GET, C.HTTP_METHOD_POST): | |
1727 # only HTTP GET and POST are handled so far | |
1728 d.addCallback(lambda __: self.pageError(request, C.HTTP_BAD_REQUEST)) | |
1729 | |
1730 if request.method == C.HTTP_METHOD_POST: | |
1731 if self.on_data_post is None: | |
1732 # if we don't have on_data_post, the page was not expecting POST | |
1733 # so we return an error | |
1734 d.addCallback(lambda __: self.pageError(request, C.HTTP_BAD_REQUEST)) | |
1735 else: | |
1736 d.addCallback(self._on_data_post, request) | |
1737 # by default, POST follow normal behaviour after on_data_post is called | |
1738 # this can be changed by a redirection or other method call in on_data_post | |
1739 | |
1740 if self.dynamic: | |
1741 d.addCallback(self._prepare_dynamic, request) | |
1742 | |
1743 if self.prepare_render: | |
1744 d.addCallback(self._prepare_render, request) | |
1745 | |
1746 if self.template: | |
1747 d.addCallback(self._render_template, request) | |
1748 elif self.render_method: | |
1749 d.addCallback(self._render_method, request) | |
1750 | |
1751 d.addCallback(self.writeData, request) | |
1752 d.addErrback(self._renderEb, request) | |
1753 d.addErrback(self._internalError, request) | |
1754 d.callback(self) | |
1755 return server.NOT_DONE_YET | 1770 return server.NOT_DONE_YET |
1756 | 1771 |
1757 def render_GET(self, request): | |
1758 return self.renderPage(request) | |
1759 | |
1760 def render_POST(self, request): | 1772 def render_POST(self, request): |
1761 return self.renderPage(request) | 1773 defer.ensureDeferred(self.renderPage(request)) |
1774 return server.NOT_DONE_YET |