[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