Mercurial > libervia-web
comparison src/server/server.py @ 917:86563d6c83b0
server: Libervia pages:
- introduce new LiberviaPage which are easy to create web pages tailored for SàT architecture. They are expected to be the cornerstone of the future of Libervia.
- webpages paths reflected by files paths in src/pages directory (directory should be changeable in the future)
- if a pages subdirectory as a page_meta.py file, it is a webpage, same for sub-subdirectories and so on
- page_meta.py variable are used to instanciate the resource. Check __init__ docstring for details
- access is set with a page_meta.py variable (only public for now), and subdirectories access are restricted by parent directory ones.
- callables in page_meta.py get 2 arguments: the LiberviaPage resource instance, and the request
- callables there can use Deferred (or not)
- LiberviaPage has a couple of helper method to e.g. parse URL or return an error
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 05 Mar 2017 23:56:31 +0100 |
parents | e9e9d9d893a8 |
children | 7b267496da1d |
comparison
equal
deleted
inserted
replaced
916:e9bb7257d051 | 917:86563d6c83b0 |
---|---|
23 from twisted.web import static | 23 from twisted.web import static |
24 from twisted.web import resource as web_resource | 24 from twisted.web import resource as web_resource |
25 from twisted.web import util as web_util | 25 from twisted.web import util as web_util |
26 from twisted.web import http | 26 from twisted.web import http |
27 from twisted.python.components import registerAdapter | 27 from twisted.python.components import registerAdapter |
28 from twisted.python.failure import Failure | 28 from twisted.python import failure |
29 from twisted.words.protocols.jabber import jid | 29 from twisted.words.protocols.jabber import jid |
30 | 30 |
31 from txjsonrpc.web import jsonrpc | 31 from txjsonrpc.web import jsonrpc |
32 from txjsonrpc import jsonrpclib | 32 from txjsonrpc import jsonrpclib |
33 | 33 |
36 from sat_frontends.bridge.dbus_bridge import Bridge, BridgeExceptionNoService, const_TIMEOUT as BRIDGE_TIMEOUT | 36 from sat_frontends.bridge.dbus_bridge import Bridge, BridgeExceptionNoService, const_TIMEOUT as BRIDGE_TIMEOUT |
37 from sat.core.i18n import _, D_ | 37 from sat.core.i18n import _, D_ |
38 from sat.core import exceptions | 38 from sat.core import exceptions |
39 from sat.tools import utils | 39 from sat.tools import utils |
40 from sat.tools.common import regex | 40 from sat.tools.common import regex |
41 from sat.tools.common import template | |
41 | 42 |
42 import re | 43 import re |
43 import glob | 44 import glob |
44 import os.path | 45 import os.path |
45 import sys | 46 import sys |
345 if len(args) != 1: | 346 if len(args) != 1: |
346 Exception("Multiple return arguments not supported") | 347 Exception("Multiple return arguments not supported") |
347 d.callback(args[0]) | 348 d.callback(args[0]) |
348 | 349 |
349 def _errback(result): | 350 def _errback(result): |
350 d.errback(Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, result.classname))) | 351 d.errback(failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, result.classname))) |
351 | 352 |
352 kwargs["callback"] = _callback | 353 kwargs["callback"] = _callback |
353 kwargs["errback"] = _errback | 354 kwargs["errback"] = _errback |
354 getattr(self.sat_host.bridge, method_name)(*args, **kwargs) | 355 getattr(self.sat_host.bridge, method_name)(*args, **kwargs) |
355 return d | 356 return d |
763 raise exceptions.PermissionError("Trying to access unallowed data (hack attempt ?)") | 764 raise exceptions.PermissionError("Trying to access unallowed data (hack attempt ?)") |
764 profile = ISATSession(self.session).profile | 765 profile = ISATSession(self.session).profile |
765 try: | 766 try: |
766 return self.sat_host.bridge.getEntitiesData(jids, keys, profile) | 767 return self.sat_host.bridge.getEntitiesData(jids, keys, profile) |
767 except Exception as e: | 768 except Exception as e: |
768 raise Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e))) | 769 raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e))) |
769 | 770 |
770 def jsonrpc_getEntityData(self, jid, keys): | 771 def jsonrpc_getEntityData(self, jid, keys): |
771 """Get cached data for an entity | 772 """Get cached data for an entity |
772 | 773 |
773 @param jid: jid of contact from who we want data | 774 @param jid: jid of contact from who we want data |
777 raise exceptions.PermissionError("Trying to access unallowed data (hack attempt ?)") | 778 raise exceptions.PermissionError("Trying to access unallowed data (hack attempt ?)") |
778 profile = ISATSession(self.session).profile | 779 profile = ISATSession(self.session).profile |
779 try: | 780 try: |
780 return self.sat_host.bridge.getEntityData(jid, keys, profile) | 781 return self.sat_host.bridge.getEntityData(jid, keys, profile) |
781 except Exception as e: | 782 except Exception as e: |
782 raise Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e))) | 783 raise failure.Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e))) |
783 | 784 |
784 def jsonrpc_getCard(self, jid_): | 785 def jsonrpc_getCard(self, jid_): |
785 """Get VCard for entiry | 786 """Get VCard for entiry |
786 @param jid_: jid of contact from who we want data | 787 @param jid_: jid of contact from who we want data |
787 @return: id to retrieve the profile""" | 788 @return: id to retrieve the profile""" |
1383 | 1384 |
1384 with open(filepath, 'w') as f: | 1385 with open(filepath, 'w') as f: |
1385 f.write(request.args[self.NAME][0]) | 1386 f.write(request.args[self.NAME][0]) |
1386 | 1387 |
1387 def finish(d): | 1388 def finish(d): |
1388 error = isinstance(d, Exception) or isinstance(d, Failure) | 1389 error = isinstance(d, Exception) or isinstance(d, failure.Failure) |
1389 request.write(C.UPLOAD_KO if error else C.UPLOAD_OK) | 1390 request.write(C.UPLOAD_KO if error else C.UPLOAD_OK) |
1390 # TODO: would be great to re-use the original Exception class and message | 1391 # TODO: would be great to re-use the original Exception class and message |
1391 # but it is lost in the middle of the backtrace and encapsulated within | 1392 # but it is lost in the middle of the backtrace and encapsulated within |
1392 # a DBusException instance --> extract the data from the backtrace? | 1393 # a DBusException instance --> extract the data from the backtrace? |
1393 request.finish() | 1394 request.finish() |
1430 """ | 1431 """ |
1431 profile = ISATSession(request.getSession()).profile | 1432 profile = ISATSession(request.getSession()).profile |
1432 return ("setAvatar", filepath, profile) | 1433 return ("setAvatar", filepath, profile) |
1433 | 1434 |
1434 | 1435 |
1436 class LiberviaPage(web_resource.Resource): | |
1437 isLeaf = True # we handle subpages ourself | |
1438 named_pages = {} | |
1439 | |
1440 def __init__(self, host, root_dir, name=None, access=None, parse_url=None, | |
1441 prepare_render=None, render=None, template=None): | |
1442 """initiate LiberviaPages | |
1443 | |
1444 LiberviaPages are the main resources of Libervia, using easy to set python files | |
1445 The arguments are the variables found in page_meta.py | |
1446 @param host(Libervia): the running instance of Libervia | |
1447 @param root_dir(unicode): aboslute path of the page | |
1448 @param name(unicode, None): if not None, a unique name to identify the page | |
1449 can then be used for e.g. redirection | |
1450 @param access(unicode, None): permission needed to access the page | |
1451 None means public access. | |
1452 Pages inherit from parent pages: e.g. if a "settings" page is restricted to admins, | |
1453 and if "settings/blog" is public, it still can only be accessed by admins. | |
1454 see C.PAGES_ACCESS_* for details | |
1455 @param parse_url(callable, None): if set it will be called to handle the URL path | |
1456 after this method, the page will be rendered if noting is left in path (request.postpath) | |
1457 else a the request will be transmitted to a subpage | |
1458 @param prepare_render(callable, None): if set, will be used to prepare the rendering | |
1459 that often means gathering data using the bridge | |
1460 @param render(callable, None): if not template is set, this method will be called and | |
1461 what it returns will be rendered. | |
1462 This method is mutually exclusive with template and must return a unicode string. | |
1463 @param template(unicode, None): path to the template to render. | |
1464 This method is mutually exclusive with render | |
1465 """ | |
1466 | |
1467 web_resource.Resource.__init__(self) | |
1468 self.host = host | |
1469 self.root_dir = root_dir | |
1470 if name is not None: | |
1471 if name in self.named_pages: | |
1472 raise exceptions.ConflictError(_(u'a Libervia page named "{}" already exists'.format(name))) | |
1473 self.named_pages[name] = self | |
1474 if access is None: | |
1475 access = C.PAGES_ACCESS_PUBLIC | |
1476 if access != C.PAGES_ACCESS_PUBLIC: | |
1477 raise NotImplementedError(u"Non public access are not implemented yet") | |
1478 self.parse_url = parse_url | |
1479 self.prepare_render = prepare_render | |
1480 self.template = template | |
1481 self.render_method = render | |
1482 if template is None: | |
1483 if not callable(render): | |
1484 log.error(_(u"render must be implemented and callable if template is not set")) | |
1485 else: | |
1486 if render is not None: | |
1487 log.error(_(u"render can't be used at the same time as template")) | |
1488 if parse_url is not None and not callable(parse_url): | |
1489 log.error(_(u"parse_url must be a callable")) | |
1490 | |
1491 @staticmethod | |
1492 def importPages(host, parent=None, path=None): | |
1493 if path is None: | |
1494 path = [] | |
1495 if parent is None: | |
1496 root_dir = os.path.join(os.path.dirname(libervia.__file__), C.PAGES_DIR) | |
1497 parent = host | |
1498 else: | |
1499 root_dir = parent.root_dir | |
1500 for d in os.listdir(root_dir): | |
1501 dir_path = os.path.join(root_dir, d) | |
1502 if not os.path.isdir(dir_path): | |
1503 continue | |
1504 meta_path = os.path.join(dir_path, C.PAGES_META_FILE) | |
1505 if os.path.isfile(meta_path): | |
1506 page_data = {} | |
1507 # we don't want to force the presence of __init__.py | |
1508 # so we use execfile instead of import. | |
1509 # TODO: when moved to Python 3, __init__.py is not mandatory anymore | |
1510 # so we can switch to import | |
1511 execfile(meta_path, page_data) | |
1512 resource = LiberviaPage( | |
1513 host, | |
1514 dir_path, | |
1515 name=page_data.get('name'), | |
1516 access=page_data.get('access'), | |
1517 parse_url=page_data.get('parse_url'), | |
1518 prepare_render=page_data.get('prepare_render'), | |
1519 render=page_data.get('render'), | |
1520 template=page_data.get('template')) | |
1521 parent.putChild(d, resource) | |
1522 new_path = path + [d] | |
1523 log.info(u"Added /{path} page".format(path=u'[...]/'.join(new_path))) | |
1524 LiberviaPage.importPages(host, resource, new_path) | |
1525 | |
1526 def getChildWithDefault(self, path, request): | |
1527 # we handle children ourselves | |
1528 raise exceptions.InternalError(u"this method should not be used with LiberviaPage") | |
1529 | |
1530 def nextPath(self, request): | |
1531 """get next URL path segment, and update request accordingly | |
1532 | |
1533 will move first segment of postpath in prepath | |
1534 @param request(server.Request): current HTTP request | |
1535 @return (unicode): unquoted segment | |
1536 @raise IndexError: there is no segment left | |
1537 """ | |
1538 pathElement = request.postpath.pop(0) | |
1539 request.prepath.append(pathElement) | |
1540 return urllib.unquote(pathElement).decode('utf-8') | |
1541 | |
1542 def pageError(self, request, code=404): | |
1543 """generate an error page and terminate the request | |
1544 | |
1545 @param request(server.Request): HTTP request | |
1546 @param core(int): error code to use | |
1547 """ | |
1548 template = u'error/' + unicode(code) + '.html' | |
1549 | |
1550 request.setResponseCode(code) | |
1551 | |
1552 rendered = self.host.renderer.render( | |
1553 template, | |
1554 root_path = '/templates/', | |
1555 error_code = code, | |
1556 **request.template_data) | |
1557 | |
1558 self.writeData(rendered, request) | |
1559 raise failure.Failure(exceptions.CancelError(u'error page is used')) | |
1560 | |
1561 def writeData(self, data, request): | |
1562 if data is None: | |
1563 self.pageError(request) | |
1564 request.write(data.encode('utf-8')) | |
1565 request.finish() | |
1566 | |
1567 def _subpagesHandler(self, dummy, request): | |
1568 """render subpage if suitable | |
1569 | |
1570 this method checks if there is still an unmanaged part of the path | |
1571 and check if it corresponds to a subpage. If so, it render the subpage | |
1572 else it render a NoResource. | |
1573 If there is no unmanaged part of the segment, current page workflow is pursued | |
1574 """ | |
1575 if request.postpath: | |
1576 subpage = self.nextPath(request) | |
1577 try: | |
1578 child = self.children[subpage] | |
1579 except KeyError: | |
1580 self.pageError(request) | |
1581 else: | |
1582 child.render(request) | |
1583 raise failure.Failure(exceptions.CancelError(u'subpage page is used')) | |
1584 | |
1585 def _prepare_render(self, dummy, request): | |
1586 defer.maybeDeferred(self.prepare_render, self, request) | |
1587 | |
1588 def _render_method(self, dummy, request): | |
1589 return defer.maybeDeferred(self.render_method, self, request) | |
1590 | |
1591 def _render_template(self, dummy, template_data): | |
1592 return self.host.renderer.render( | |
1593 self.template, | |
1594 root_path = '/templates/', | |
1595 **template_data) | |
1596 | |
1597 def _renderEb(self, failure_, request): | |
1598 """don't raise error on CancelError""" | |
1599 failure_.trap(exceptions.CancelError) | |
1600 | |
1601 def render_GET(self, request): | |
1602 if not hasattr(request, 'template_data'): | |
1603 request.template_data = {} | |
1604 if request.postpath and not request.postpath[-1]: | |
1605 # we don't differenciate URLs finishing with '/' or not | |
1606 del request.postpath[-1] | |
1607 d = defer.Deferred() | |
1608 if self.parse_url is not None: | |
1609 d.addCallback(self.parse_url, request) | |
1610 | |
1611 d.addCallback(self._subpagesHandler, request) | |
1612 | |
1613 if self.prepare_render: | |
1614 d.addCallback(self._prepare_render, request) | |
1615 | |
1616 if self.template: | |
1617 d.addCallback(self._render_template, request.template_data) | |
1618 elif self.render_method: | |
1619 d.addCallback(self._render_method, request) | |
1620 | |
1621 d.addCallback(self.writeData, request) | |
1622 d.addErrback(self._renderEb, request) | |
1623 d.callback(self) | |
1624 return server.NOT_DONE_YET | |
1625 | |
1626 | |
1435 class Libervia(service.Service): | 1627 class Libervia(service.Service): |
1436 | |
1437 | 1628 |
1438 def __init__(self, options): | 1629 def __init__(self, options): |
1439 self.options = options | 1630 self.options = options |
1440 self.initialised = defer.Deferred() | 1631 self.initialised = defer.Deferred() |
1441 | 1632 |
1509 | 1700 |
1510 # static pages | 1701 # static pages |
1511 self.putChild('blog', MicroBlog(self)) | 1702 self.putChild('blog', MicroBlog(self)) |
1512 self.putChild(C.THEMES_URL, ProtectedFile(self.themes_dir)) | 1703 self.putChild(C.THEMES_URL, ProtectedFile(self.themes_dir)) |
1513 | 1704 |
1705 LiberviaPage.importPages(self) | |
1706 | |
1514 # media dirs | 1707 # media dirs |
1515 # FIXME: get rid of dirname and "/" in C.XXX_DIR | 1708 # FIXME: get rid of dirname and "/" in C.XXX_DIR |
1516 self.putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir)) | 1709 self.putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir)) |
1517 self.cache_resource = web_resource.NoResource() | 1710 self.cache_resource = web_resource.NoResource() |
1518 self.putChild(C.CACHE_DIR, self.cache_resource) | 1711 self.putChild(C.CACHE_DIR, self.cache_resource) |
1525 | 1718 |
1526 | 1719 |
1527 wrapped = web_resource.EncodingResourceWrapper(root, [server.GzipEncoderFactory()]) | 1720 wrapped = web_resource.EncodingResourceWrapper(root, [server.GzipEncoderFactory()]) |
1528 self.site = server.Site(wrapped) | 1721 self.site = server.Site(wrapped) |
1529 self.site.sessionFactory = LiberviaSession | 1722 self.site.sessionFactory = LiberviaSession |
1723 self.renderer = template.Renderer(self) | |
1724 self.putChild('templates', ProtectedFile(self.renderer.base_dir)) | |
1530 | 1725 |
1531 | 1726 |
1532 def _bridgeCb(self): | 1727 def _bridgeCb(self): |
1533 self.bridge.getReady(lambda: self.initialised.callback(None), | 1728 self.bridge.getReady(lambda: self.initialised.callback(None), |
1534 lambda failure: self.initialised.errback(Exception(failure))) | 1729 lambda failure: self.initialised.errback(Exception(failure))) |