[CalendarServer-changes] [5593] CalendarServer/branches/users/cdaboo/more-sharing-5591/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Wed May 12 19:40:20 PDT 2010


Revision: 5593
          http://trac.macosforge.org/projects/calendarserver/changeset/5593
Author:   cdaboo at apple.com
Date:     2010-05-12 19:40:18 -0700 (Wed, 12 May 2010)
Log Message:
-----------
Support "direct" sharing by allowing creation of a share via GET request using ?action=share query param.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/more-sharing-5591/twistedcaldav/method/get.py
    CalendarServer/branches/users/cdaboo/more-sharing-5591/twistedcaldav/resource.py
    CalendarServer/branches/users/cdaboo/more-sharing-5591/twistedcaldav/sharing.py

Modified: CalendarServer/branches/users/cdaboo/more-sharing-5591/twistedcaldav/method/get.py
===================================================================
--- CalendarServer/branches/users/cdaboo/more-sharing-5591/twistedcaldav/method/get.py	2010-05-13 02:32:54 UTC (rev 5592)
+++ CalendarServer/branches/users/cdaboo/more-sharing-5591/twistedcaldav/method/get.py	2010-05-13 02:40:18 UTC (rev 5593)
@@ -21,7 +21,9 @@
 __all__ = ["http_GET"]
 
 from twisted.internet.defer import inlineCallbacks, returnValue
+from twext.web2 import responsecode
 from twext.web2.dav import davxml
+from twext.web2.dav.http import ErrorResponse
 from twext.web2.dav.util import parentForURL
 from twext.web2.http import HTTPError
 from twext.web2.http import Response
@@ -29,48 +31,70 @@
 from twext.web2.stream import MemoryStream
 
 from twistedcaldav.caldavxml import ScheduleTag
-from twistedcaldav.customxml import TwistedCalendarAccessProperty
+from twistedcaldav.customxml import TwistedCalendarAccessProperty,\
+    calendarserver_namespace
 from twistedcaldav.datafilters.privateevents import PrivateEventFilter
 from twistedcaldav.resource import isPseudoCalendarCollectionResource
 
 @inlineCallbacks
 def http_GET(self, request):
 
-    # Look for calendar access restriction on existing resource.
     if self.exists():
-        parentURL = parentForURL(request.uri)
-        parent = (yield request.locateResource(parentURL))
-        if isPseudoCalendarCollectionResource(parent):
-    
-            # Check authorization first
-            yield self.authorize(request, (davxml.Read(),))
-
-            caldata = (yield self.iCalendarForUser(request))
-
-            try:
-                access = self.readDeadProperty(TwistedCalendarAccessProperty)
-            except HTTPError:
-                access = None
+        # Special sharing request on a calendar or address book
+        if self.isCalendarCollection() or self.isAddressBookCollection():
+            
+            # Check for action=share
+            if request.args:
+                action = request.args.get("action", ("",))
+                if len(action) != 1:
+                    raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-action")))
+                action = action[0]
+                    
+                dispatch = {
+                    "share"   : self.directShare,
+                }.get(action, None)
                 
-            if access:
+                if dispatch is None:
+                    raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "supported-action")))
         
-                # Non DAV:owner's have limited access to the data
-                isowner = (yield self.isOwner(request, adminprincipals=True, readprincipals=True))
-                
-                # Now "filter" the resource calendar data
-                caldata = PrivateEventFilter(access, isowner).filter(caldata)
+                response = (yield dispatch(request))
+                returnValue(response)
+        
+        else:
+            # Look for calendar access restriction on existing resource.
+            parentURL = parentForURL(request.uri)
+            parent = (yield request.locateResource(parentURL))
+            if isPseudoCalendarCollectionResource(parent):
+        
+                # Check authorization first
+                yield self.authorize(request, (davxml.Read(),))
     
-            response = Response()
-            response.stream = MemoryStream(str(caldata))
-            response.headers.setHeader("content-type", MimeType.fromString("text/calendar; charset=utf-8"))
+                caldata = (yield self.iCalendarForUser(request))
     
-            # Add Schedule-Tag header if property is present
-            if self.hasDeadProperty(ScheduleTag):
-                scheduletag = self.readDeadProperty(ScheduleTag)
-                if scheduletag:
-                    response.headers.setHeader("Schedule-Tag", str(scheduletag))
+                try:
+                    access = self.readDeadProperty(TwistedCalendarAccessProperty)
+                except HTTPError:
+                    access = None
+                    
+                if access:
+            
+                    # Non DAV:owner's have limited access to the data
+                    isowner = (yield self.isOwner(request, adminprincipals=True, readprincipals=True))
+                    
+                    # Now "filter" the resource calendar data
+                    caldata = PrivateEventFilter(access, isowner).filter(caldata)
         
-            returnValue(response)
+                response = Response()
+                response.stream = MemoryStream(str(caldata))
+                response.headers.setHeader("content-type", MimeType.fromString("text/calendar; charset=utf-8"))
+        
+                # Add Schedule-Tag header if property is present
+                if self.hasDeadProperty(ScheduleTag):
+                    scheduletag = self.readDeadProperty(ScheduleTag)
+                    if scheduletag:
+                        response.headers.setHeader("Schedule-Tag", str(scheduletag))
+            
+                returnValue(response)
 
     # Do normal GET behavior
     response = (yield super(CalDAVFile, self).http_GET(request))

Modified: CalendarServer/branches/users/cdaboo/more-sharing-5591/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/more-sharing-5591/twistedcaldav/resource.py	2010-05-13 02:32:54 UTC (rev 5592)
+++ CalendarServer/branches/users/cdaboo/more-sharing-5591/twistedcaldav/resource.py	2010-05-13 02:40:18 UTC (rev 5593)
@@ -506,10 +506,12 @@
     @inlineCallbacks
     def accessControlList(self, request, *args, **kwargs):
 
+        acls = None
         isvirt = (yield self.isVirtualShare(request))
         if isvirt:
             acls = self.shareeAccessControlList()
-        else:
+
+        if acls is None:
             acls = (yield super(CalDAVResource, self).accessControlList(request, *args, **kwargs))
 
         # Look for private events access classification

Modified: CalendarServer/branches/users/cdaboo/more-sharing-5591/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/more-sharing-5591/twistedcaldav/sharing.py	2010-05-13 02:32:54 UTC (rev 5592)
+++ CalendarServer/branches/users/cdaboo/more-sharing-5591/twistedcaldav/sharing.py	2010-05-13 02:40:18 UTC (rev 5593)
@@ -29,7 +29,7 @@
     returnValue
 from twistedcaldav import customxml, caldavxml
 from twistedcaldav.config import config
-from twistedcaldav.customxml import SharedCalendar
+from twistedcaldav.customxml import SharedCalendar, calendarserver_namespace
 from twistedcaldav.sql import AbstractSQLDatabase, db_prefix
 from uuid import uuid4
 from vobject.icalendar import dateTimeToString, utc
@@ -41,6 +41,10 @@
 Sharing behavior
 """
 
+# Types of sharing mode
+SHARETYPE_INVITE = "I"  # Invite based sharing
+SHARETYPE_DIRECT = "D"  # Direct linking based sharing
+
 class SharedCollectionMixin(object):
     
     def invitesDB(self):
@@ -132,6 +136,73 @@
                 record.summary = summary
             self.invitesDB().addOrUpdateRecord(record)
 
+    @inlineCallbacks
+    def directShare(self, request):
+        """
+        Directly bind an accessible calendar/address book collection into the current
+        principal's calendar/addressbook home.
+
+        @param request: the request triggering this action
+        @type request: L{IRequest}
+        """
+        
+        # Need to have at least DAV:read to do this
+        yield self.authorize(request, (davxml.Read(),))
+        
+        # Find current principal
+        authz_principal = self.currentPrincipal(request).children[0]
+        if not isinstance(authz_principal, davxml.HRef):
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (calendarserver_namespace, "valid-principal"),
+                "Current user principal not a DAV:href",
+            ))
+        principalURL = str(authz_principal)
+        if not principalURL:
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (calendarserver_namespace, "valid-principal"),
+                "Current user principal not specified",
+            ))
+        principal = (yield request.locateResource(principalURL))
+        
+        # Check enabled for service
+        from twistedcaldav.directory.principal import DirectoryCalendarPrincipalResource
+        if not isinstance(principal, DirectoryCalendarPrincipalResource):
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (calendarserver_namespace, "invalid-principal"),
+                "Current user principal is not a calendar/addressbook enabled principal",
+            ))
+        
+        # Get the home collection
+        if self.isCalendarCollection():
+            home = principal.calendarHome()
+        elif self.isAddressBookCollection():
+            home = principal.addressBookHome()
+        else:
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (calendarserver_namespace, "invalid-principal"),
+                "No calendar/addressbook home for principal",
+            ))
+            
+        # TODO: Make sure principal is not sharing back to themselves
+        compareURL = (yield self.canonicalURL(request))
+        homeURL = home.url()
+        if compareURL.startswith(homeURL):
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (calendarserver_namespace, "invalid-share"),
+                "Can't share your own calendar or addressbook",
+            ))
+
+        # Accept it
+        response = (yield home.acceptDirectShare(request, request.path, self.resourceID(), self.displayName()))
+        
+        # Return the URL of the shared calendar
+        returnValue(response)
+
     def isShared(self, request):
         """ Return True if this is an owner shared calendar collection """
         return succeed(self.isSpecialCollection(customxml.SharedOwner))
@@ -182,8 +253,14 @@
 
         assert self._isVirtualShare, "Only call this fort a virtual share"
 
+        # Direct shares use underlying privileges of shared collection
+        if self._share.sharetype == SHARETYPE_DIRECT:
+            return None
+
+        # Invite shares use access mode from the invite
+
         # Get the invite for this sharee
-        invite = self.invitesDB().recordForInviteUID(self._share.inviteuid)
+        invite = self.invitesDB().recordForInviteUID(self._share.shareuid)
         if invite is None:
             return davxml.ACL()
         
@@ -829,15 +906,25 @@
             self._provisionedShares = True
 
     @inlineCallbacks
-    def acceptShare(self, request, hostUrl, inviteUID, displayname=None):
+    def acceptInviteShare(self, request, hostUrl, inviteUID, displayname=None):
         
-        # Do this first to make sure we have a valid share
+        # Send the invite reply then add the link
         yield self._changeShare(request, "ACCEPTED", hostUrl, inviteUID, displayname)
 
+        yield self._acceptShare(request, SHARETYPE_INVITE, hostUrl, inviteUID, displayname)
+
+    def acceptDirectShare(self, request, hostUrl, resourceUID, displayname=None):
+
+        # Just add the link
+        return self._acceptShare(request, SHARETYPE_DIRECT, hostUrl, resourceUID, displayname)
+
+    @inlineCallbacks
+    def _acceptShare(self, request, sharetype, hostUrl, shareUID, displayname=None):
+
         # Add or update in DB
-        oldShare = self.sharesDB().recordForInviteUID(inviteUID)
+        oldShare = self.sharesDB().recordForShareUID(shareUID)
         if not oldShare:
-            oldShare = share = SharedCalendarRecord(inviteUID, hostUrl, str(uuid4()), displayname)
+            oldShare = share = SharedCalendarRecord(shareUID, sharetype, hostUrl, str(uuid4()), displayname)
             self.sharesDB().addOrUpdateRecord(share)
         
         # Set per-user displayname to whatever was given
@@ -860,32 +947,43 @@
 
     def removeShare(self, request, share):
         """ Remove a shared calendar named in resourceName and send a decline """
-        return self.declineShare(request, share.hosturl, share.inviteuid)
 
+        # Send a decline when an invite share is removed only
+        if share.sharetype == SHARETYPE_INVITE:
+            return self.declineShare(request, share.hosturl, share.shareuid)
+        else:
+            self.removeDirectShare(request, share)
+
     @inlineCallbacks
-    def removeShareByUID(self, request, inviteuid):
+    def removeShareByUID(self, request, shareUID):
         """ Remove a shared calendar but do not send a decline back """
 
-        record = self.sharesDB().recordForInviteUID(inviteuid)
-        if record:
-            shareURL = joinURL(self.url(), record.localname)
-    
-            # For backwards compatibility we need to sync this up with the calendar-free-busy-set on the inbox
-            principal = (yield self.resourceOwnerPrincipal(request))
-            inboxURL = principal.scheduleInboxURL()
-            if inboxURL:
-                inbox = (yield request.locateResource(inboxURL))
-                inbox.processFreeBusyCalendar(shareURL, False)
-    
-            self.sharesDB().removeRecordForInviteUID(inviteuid)
+        share = self.sharesDB().recordForShareUID(shareUID)
+        if share:
+            yield self.removeDirectShare(request, share)
 
         returnValue(True)
 
     @inlineCallbacks
+    def removeDirectShare(self, request, share):
+        """ Remove a shared calendar but do not send a decline back """
+
+        shareURL = joinURL(self.url(), share.localname)
+
+        # For backwards compatibility we need to sync this up with the calendar-free-busy-set on the inbox
+        principal = (yield self.resourceOwnerPrincipal(request))
+        inboxURL = principal.scheduleInboxURL()
+        if inboxURL:
+            inbox = (yield request.locateResource(inboxURL))
+            inbox.processFreeBusyCalendar(shareURL, False)
+
+        self.sharesDB().removeRecordForShareUID(share.shareuid)
+
+    @inlineCallbacks
     def declineShare(self, request, hostUrl, inviteUID):
 
         # Remove it if its in the DB
-        self.sharesDB().removeRecordForInviteUID(inviteUID)
+        yield self.removeShareByUID(inviteUID)
 
         yield self._changeShare(request, "DECLINED", hostUrl, inviteUID)
         
@@ -976,7 +1074,7 @@
                     "missing required XML elements",
                 ))
             if accepted:
-                return self.acceptShare(request, hostUrl, replytoUID, displayname=summary)
+                return self.acceptInviteShare(request, hostUrl, replytoUID, displayname=summary)
             else:
                 return self.declineShare(request, hostUrl, replytoUID)
 
@@ -1001,8 +1099,9 @@
 
 class SharedCalendarRecord(object):
     
-    def __init__(self, inviteuid, hosturl, localname, summary):
-        self.inviteuid = inviteuid
+    def __init__(self, shareuid, sharetype, hosturl, localname, summary):
+        self.shareuid = shareuid
+        self.sharetype = sharetype
         self.hosturl = hosturl
         self.localname = localname
         self.summary = summary
@@ -1038,25 +1137,25 @@
         row = self._db_execute("select * from SHARES where LOCALNAME = :1", localname)
         return self._makeRecord(row[0]) if row else None
     
-    def recordForInviteUID(self, inviteUID):
+    def recordForShareUID(self, shareUID):
 
-        row = self._db_execute("select * from SHARES where INVITEUID = :1", inviteUID)
+        row = self._db_execute("select * from SHARES where SHAREUID = :1", shareUID)
         return self._makeRecord(row[0]) if row else None
     
     def addOrUpdateRecord(self, record):
 
-        self._db_execute("""insert or replace into SHARES (INVITEUID, HOSTURL, LOCALNAME, SUMMARY)
-            values (:1, :2, :3, :4)
-            """, record.inviteuid, record.hosturl, record.localname, record.summary,
+        self._db_execute("""insert or replace into SHARES (SHAREUID, SHARETYPE, HOSTURL, LOCALNAME, SUMMARY)
+            values (:1, :2, :3, :4, :5)
+            """, record.shareuid, record.sharetype, record.hosturl, record.localname, record.summary,
         )
     
     def removeRecordForLocalName(self, localname):
 
         self._db_execute("delete from SHARES where LOCALNAME = :1", localname)
     
-    def removeRecordForInviteUID(self, inviteUID):
+    def removeRecordForShareUID(self, shareUID):
 
-        self._db_execute("delete from SHARES where INVITEUID = :1", inviteUID)
+        self._db_execute("delete from SHARES where SHAREUID = :1", shareUID)
     
     def remove(self):
         
@@ -1082,15 +1181,17 @@
         """
         #
         # SHARES table is the primary table
-        #   INVITEUID: UID for this invite
+        #   SHAREUID: UID for this share
+        #   SHARETYPE: type of share: "I" for invite, "D" for direct
         #   HOSTURL: URL for data source
         #   LOCALNAME: local path name
-        #   SUMMARY: Invite summary
+        #   SUMMARY: Share summary
         #
         q.execute(
             """
             create table SHARES (
-                INVITEUID      text unique,
+                SHAREUID       text unique,
+                SHARETYPE      text(1),
                 HOSTURL        text,
                 LOCALNAME      text,
                 SUMMARY        text
@@ -1100,7 +1201,7 @@
 
         q.execute(
             """
-            create index INVITEUID on SHARES (INVITEUID)
+            create index SHAREUID on SHARES (SHAREUID)
             """
         )
         q.execute(
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100512/10778f9c/attachment-0001.html>


More information about the calendarserver-changes mailing list