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)))