implement principal resources
authorchrysn <chrysn@fsfe.org>
Fri, 4 Apr 2014 08:11:21 +0000 (10:11 +0200)
committerJelmer Vernooń≥ <jelmer@jelmer.uk>
Sun, 10 Apr 2016 11:43:29 +0000 (11:43 +0000)
this makes calypso usable with davdroid and play nicer with acal, but
still has shortcomings:

* only one hard-coded resource per user and type listed (no enumeration)
* code organization is half-hearted object-style vs switch-style

calypso/__init__.py
calypso/principal.py [new file with mode: 0644]
calypso/webdav.py
calypso/xmlutils.py

index 28e29f4..10723c1 100644 (file)
@@ -132,6 +132,13 @@ class HTTPSServer(HTTPServer):
         self.server_bind()
         self.server_activate()
 
+def collection_singleton(p):
+    path = paths.collection_from_path(p)
+    if not path:
+        return None
+    if not path in CollectionHTTPHandler.collections:
+        CollectionHTTPHandler.collections[path] = webdav.Collection(path)
+    return CollectionHTTPHandler.collections[path]
 
 class CollectionHTTPHandler(server.BaseHTTPRequestHandler):
     """HTTP requests handler for WebDAV collections."""
@@ -250,12 +257,7 @@ class CollectionHTTPHandler(server.BaseHTTPRequestHandler):
     @property
     def _collection(self):
         """The ``webdav.Collection`` object corresponding to the given path."""
-        path = paths.collection_from_path(self.path)
-        if not path:
-            return None
-        if not path in CollectionHTTPHandler.collections:
-            CollectionHTTPHandler.collections[path] = webdav.Collection(path)
-        return CollectionHTTPHandler.collections[path]
+        return collection_singleton(self.path)
 
     def _decode(self, text):
         """Try to decode text according to various parameters."""
diff --git a/calypso/principal.py b/calypso/principal.py
new file mode 100644 (file)
index 0000000..42447a9
--- /dev/null
@@ -0,0 +1,65 @@
+import xml.etree.ElementTree as ET
+from . import config, paths
+from .xmlutils_generic import _tag
+
+class Resource(object):
+    """Resources initially were pseudo-collections/items (so they could be
+    included in the propfind loop), but currently are objects that represent
+    resources on the server that are not collectons / collection items.
+
+    Their interfaces for propfind could possibly be inherited to Collection and
+    collection Item in the future."""
+
+    def propfind(self, tag, element):
+        """If self can respond to a propfind request on a tag, update the
+        prepared response element with child nodes."""
+
+    def propfind_children(self, depth):
+        """Return a list of resources / collections / items that are to be
+        responded with to a propfind of a given depth"""
+        return [self]
+
+    urlpath = None # this should be present ... implement as abstract property?
+
+class Principal(Resource):
+    def __init__(self, username):
+        self.username = username
+        self.urlpath = config.get("server", "user_principal") % {"user": self.username} # it's currently hardcoded anyway
+
+    def propfind(self, tag, element):
+        super(Principal, self).propfind(tag, element)
+
+        # maybe move those out to generic resources; kaddressbook doesn't query
+        # for current-user-princial and ask there, but plain go to the
+        # requested url and propfind for home-sets
+        if tag == _tag("C", "calendar-home-set"):
+            tag = ET.Element(_tag("D", "href"))
+            tag.text = self.urlpath + CalendarHomeSet.type_dependent_suffix + '/'
+            element.append(tag)
+        elif tag == _tag("A", "addressbook-home-set"):
+            tag = ET.Element(_tag("D", "href"))
+            tag.text = self.urlpath + AddressbookHomeSet.type_dependent_suffix + '/'
+            element.append(tag)
+
+class HomeSet(Resource):
+    def __init__(self, username):
+        self.username = username
+        self.urlpath = config.get("server", "user_principal") % {"user": self.username} + self.type_dependent_suffix + "/" # it's currently hardcoded anyway
+
+    def propfind_children(self, depth):
+        # FIXME ignoring depth
+
+        collection_name = paths.collection_from_path(self.username + "/" + self.single_collection)
+        from calypso import collection_singleton
+        collection = collection_singleton(collection_name)
+        items = [collection] # + collection.items # FIXME sequence matters, see parentcollectionhack
+        return super(HomeSet, self).propfind_children(depth) + items
+
+class AddressbookHomeSet(HomeSet):
+    type_dependent_suffix = "addressbooks"
+    single_collection = "addresses"
+
+class CalendarHomeSet(HomeSet):
+    type_dependent_suffix = "calendars"
+    single_collection = "calendar"
+
index 3983b6c..2ddc38c 100644 (file)
@@ -333,7 +333,7 @@ class Collection(object):
         
         self.log = logging.getLogger(__name__)
         self.encoding = "utf-8"
-        self.urlpath = path
+        self.urlpath = paths.base_prefix() if path == "/" else paths.base_prefix() + path + "/" # a collections's path always end with / as recommended in rfc4918 as suggestion
         self.owner = paths.url_to_owner(path)
         self.path = paths.url_to_file(path)
         self.files = []
index be8b556..10b39bf 100644 (file)
@@ -38,7 +38,7 @@ import datetime
 import email.utils
 import logging
 
-from . import client, config, webdav, paths
+from . import client, config, webdav, paths, principal
 
 __package__ = 'calypso.xmlutils'
 
@@ -84,6 +84,33 @@ def delete(path, collection, context):
 
     return ET.tostring(multistatus, config.get("encoding", "request"))
 
+def identify_resource(path):
+    """Return a Resource object corresponding to the path (this is used for
+    everything that is not a collection, like Principal and HomeSet objects)"""
+
+    try:
+        left, right = config.get('server', 'user_principal').split('%(user)s')
+    except ValueError:
+        raise ValueError("user_principal setting must contain %(user)s.")
+
+    if not path.startswith(left):
+        return None
+
+    remainder = path[len(left):]
+    if right not in remainder:
+        return None
+
+    username = remainder[:remainder.index(right)]
+    remainder = remainder[remainder.index(right)+len(right):]
+
+    if remainder == principal.AddressbookHomeSet.type_dependent_suffix + "/":
+        return principal.AddressbookHomeSet(username)
+    elif remainder == principal.CalendarHomeSet.type_dependent_suffix + "/":
+        return principal.CalendarHomeSet(username)
+    elif remainder == "":
+        return principal.Principal(username)
+    else:
+        return None
 
 def propfind(path, xml_request, collection, depth, context):
     """Read and answer PROPFIND requests.
@@ -93,7 +120,6 @@ def propfind(path, xml_request, collection, depth, context):
     """
 
     item_name = paths.resource_from_path(path)
-    collection_name = paths.collection_from_path(path)
 
     if xml_request:
         # Reading request
@@ -122,7 +148,11 @@ def propfind(path, xml_request, collection, depth, context):
     # Writing answer
     multistatus = ET.Element(_tag("D", "multistatus"))
 
-    if collection:
+    resource = identify_resource(path)
+
+    if resource is not None:
+        items = resource.propfind_children(depth)
+    elif collection:
         if item_name:
             item = collection.get_item(item_name)
             print "item_name %s item %s" % (item_name, item)
@@ -142,12 +172,26 @@ def propfind(path, xml_request, collection, depth, context):
 
     for item in items:
         is_collection = isinstance(item, webdav.Collection)
+        is_resource = isinstance(item, principal.Resource)
+
+        if is_collection:
+            # parentcollectionhack. this is not the way to do it, but much of
+            # the below code relies on items which are collection members to
+            # have their parent collection in the collection variable. get rid
+            # of the collection propfind-global variable, and this and all
+            # other occurrences of "parentcollectionhack" can be dropped.
+            collection = item
 
         response = ET.Element(_tag("D", "response"))
         multistatus.append(response)
 
         href = ET.Element(_tag("D", "href"))
-        href.text = collection_name if is_collection else "/".join([collection_name, item.name])
+        if is_collection:
+            href.text = item.urlpath
+        elif is_resource:
+            href.text = item.urlpath
+        else:
+            href.text = collection.urlpath + item.name
         response.append(href)
 
         propstat = ET.Element(_tag("D", "propstat"))
@@ -190,8 +234,8 @@ def propfind(path, xml_request, collection, depth, context):
             elif tag in (
                 _tag("D", "principal-collection-set"),
                 _tag("C", "calendar-user-address-set"),
-                _tag("C", "calendar-home-set"),
-                _tag("A", "addressbook-home-set")):
+                ):
+                # not meaningfully implemented yet
                 tag = ET.Element(_tag("D", "href"))
                 tag.text = path
                 element.append(tag)