[CalendarServer-changes] [5308] CalendarServer/branches/users/cdaboo/shared-calendars-5187/ twistedcaldav
source_changes at macosforge.org
source_changes at macosforge.org
Mon Mar 15 12:59:17 PDT 2010
Revision: 5308
http://trac.macosforge.org/projects/calendarserver/changeset/5308
Author: cdaboo at apple.com
Date: 2010-03-15 12:59:17 -0700 (Mon, 15 Mar 2010)
Log Message:
-----------
Checkpoint for shared calendar resource support.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/directory/calendaruserproxy.py
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/directorybackedaddressbook.py
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/dropbox.py
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/extensions.py
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/freebusyurl.py
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/mail.py
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/method/delete_common.py
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/notifications.py
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/resource.py
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/schedule.py
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharing.py
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/static.py
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/test/test_sharing.py
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/timezoneservice.py
Added Paths:
-----------
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharedcalendar.py
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/directory/calendaruserproxy.py 2010-03-15 18:05:21 UTC (rev 5307)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/directory/calendaruserproxy.py 2010-03-15 19:59:17 UTC (rev 5308)
@@ -128,13 +128,13 @@
"""
return ProxyDBService
- def resourceType(self):
+ def resourceType(self, request):
if self.proxyType == "calendar-proxy-read":
- return davxml.ResourceType.calendarproxyread
+ return succeed(davxml.ResourceType.calendarproxyread)
elif self.proxyType == "calendar-proxy-write":
- return davxml.ResourceType.calendarproxywrite
+ return succeed(davxml.ResourceType.calendarproxywrite)
else:
- return super(CalendarUserProxyPrincipalResource, self).resourceType()
+ return super(CalendarUserProxyPrincipalResource, self).resourceType(request)
def isProxyType(self, read_write):
if (
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/directorybackedaddressbook.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/directorybackedaddressbook.py 2010-03-15 18:05:21 UTC (rev 5307)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/directorybackedaddressbook.py 2010-03-15 19:59:17 UTC (rev 5308)
@@ -94,8 +94,8 @@
),
)
- def resourceType(self):
- return davxml.ResourceType.addressbook #@UndefinedVariable
+ def resourceType(self, request):
+ return succeed(davxml.ResourceType.addressbook)
def isDirectoryBackedAddressBookCollection(self):
return True
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/dropbox.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/dropbox.py 2010-03-15 18:05:21 UTC (rev 5307)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/dropbox.py 2010-03-15 19:59:17 UTC (rev 5308)
@@ -32,14 +32,16 @@
from twistedcaldav.customxml import calendarserver_namespace
+from twisted.internet.defer import succeed
+
log = Logger()
class DropBoxHomeResource (DAVResource):
"""
Drop box collection resource.
"""
- def resourceType(self):
- return davxml.ResourceType.dropboxhome
+ def resourceType(self, request):
+ return succeed(davxml.ResourceType.dropboxhome)
def isCollection(self):
return True
@@ -51,8 +53,8 @@
"""
Drop box resource.
"""
- def resourceType(self):
- return davxml.ResourceType.dropbox
+ def resourceType(self, request):
+ return succeed(davxml.ResourceType.dropbox)
def isCollection(self):
return True
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/extensions.py 2010-03-15 18:05:21 UTC (rev 5307)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/extensions.py 2010-03-15 19:59:17 UTC (rev 5308)
@@ -676,7 +676,8 @@
if namespace == dav_namespace:
if name == "resourcetype":
- returnValue(self.resourceType())
+ rtype = (yield self.resourceType(request))
+ returnValue(rtype)
elif namespace == calendarserver_namespace:
if name == "expanded-group-member-set":
@@ -717,14 +718,14 @@
def expandedGroupMemberships(self):
return succeed(())
- def resourceType(self):
+ def resourceType(self, request):
# Allow live property to be overridden by dead property
if self.deadProperties().contains((dav_namespace, "resourcetype")):
- return self.deadProperties().get((dav_namespace, "resourcetype"))
+ return succeed(self.deadProperties().get((dav_namespace, "resourcetype")))
if self.isCollection():
- return davxml.ResourceType(davxml.Collection(), davxml.Principal())
+ return succeed(davxml.ResourceType(davxml.Collection(), davxml.Principal()))
else:
- return davxml.ResourceType(davxml.Principal())
+ return succeed(davxml.ResourceType(davxml.Principal()))
class DAVFile (SudoSACLMixin, SuperDAVFile, LoggingMixIn):
@@ -738,17 +739,17 @@
qname = property.qname()
if qname == (dav_namespace, "resourcetype"):
- return succeed(self.resourceType())
+ return self.resourceType(request)
return super(DAVFile, self).readProperty(property, request)
- def resourceType(self):
+ def resourceType(self, request):
# Allow live property to be overridden by dead property
if self.deadProperties().contains((dav_namespace, "resourcetype")):
- return self.deadProperties().get((dav_namespace, "resourcetype"))
+ return succeed(self.deadProperties().get((dav_namespace, "resourcetype")))
if self.isCollection():
- return davxml.ResourceType.collection
- return davxml.ResourceType.empty
+ return succeed(davxml.ResourceType.collection)
+ return succeed(davxml.ResourceType.empty)
def render(self, request):
if not self.fp.exists():
@@ -1065,38 +1066,44 @@
self.propertyStore = propertyStore
self.resource = propertyStore.resource
- def get(self, qname):
+ def get(self, qname, uid=None):
#self.log_debug("Get: %r, %r" % (self.resource.fp.path, qname))
cache = self._cache()
+
+ cachedQname = qname + (uid,)
- if qname in cache:
- property = cache.get(qname, None)
+ if cachedQname in cache:
+ property = cache.get(cachedQname, None)
if property is None:
self.log_debug("Cache miss: %r, %r, %r" % (self, self.resource.fp.path, qname))
try:
- property = self.propertyStore.get(qname)
+ property = self.propertyStore.get(qname, uid)
except HTTPError:
- del cache[qname]
+ del cache[cachedQname]
raise PropertyNotFoundError(qname)
- cache[qname] = property
+ cache[cachedQname] = property
return property
else:
raise PropertyNotFoundError(qname)
- def set(self, property):
+ def set(self, property, uid=None):
#self.log_debug("Set: %r, %r" % (self.resource.fp.path, property))
cache = self._cache()
- cache[property.qname()] = None
- self.propertyStore.set(property)
- cache[property.qname()] = property
+ cachedQname = property.qname() + (uid,)
- def contains(self, qname):
+ cache[cachedQname] = None
+ self.propertyStore.set(property, uid)
+ cache[cachedQname] = property
+
+ def contains(self, qname, uid=None):
#self.log_debug("Contains: %r, %r" % (self.resource.fp.path, qname))
+ cachedQname = qname + (uid,)
+
try:
cache = self._cache()
except HTTPError, e:
@@ -1105,30 +1112,40 @@
else:
raise
- if qname in cache:
+ if cachedQname in cache:
#self.log_debug("Contains cache hit: %r, %r, %r" % (self, self.resource.fp.path, qname))
return True
else:
return False
- def delete(self, qname):
+ def delete(self, qname, uid=None):
#self.log_debug("Delete: %r, %r" % (self.resource.fp.path, qname))
- if self._data is not None and qname in self._data:
- del self._data[qname]
+ cachedQname = qname + (uid,)
- self.propertyStore.delete(qname)
+ if self._data is not None and cachedQname in self._data:
+ del self._data[cachedQname]
- def list(self):
+ self.propertyStore.delete(qname, uid)
+
+ def list(self, uid=None, filterByUID=True):
#self.log_debug("List: %r" % (self.resource.fp.path,))
- return self._cache().iterkeys()
+ keys = self._cache().iterkeys()
+ if filterByUID:
+ return [
+ (namespace, name)
+ for namespace, name, propuid in keys
+ if propuid == uid
+ ]
+ else:
+ return keys
def _cache(self):
if not hasattr(self, "_data"):
#self.log_debug("Cache init: %r" % (self.resource.fp.path,))
self._data = dict(
(name, None)
- for name in self.propertyStore.list()
+ for name in self.propertyStore.list(filterByUID=False)
)
return self._data
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/freebusyurl.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/freebusyurl.py 2010-03-15 18:05:21 UTC (rev 5307)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/freebusyurl.py 2010-03-15 19:59:17 UTC (rev 5308)
@@ -26,7 +26,7 @@
from vobject.icalendar import utc
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
from twext.python.log import Logger
from twext.web2 import responsecode
@@ -99,8 +99,8 @@
)
return davxml.ACL(*aces)
- def resourceType(self):
- return davxml.ResourceType.freebusyurl
+ def resourceType(self, request):
+ return succeed(davxml.ResourceType.freebusyurl)
def isCollection(self):
return False
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/mail.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/mail.py 2010-03-15 18:05:21 UTC (rev 5307)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/mail.py 2010-03-15 19:59:17 UTC (rev 5308)
@@ -218,8 +218,8 @@
return succeed(self.iMIPACL)
- def resourceType(self):
- return davxml.ResourceType.ischeduleinbox
+ def resourceType(self, request):
+ return succeed(davxml.ResourceType.ischeduleinbox)
def isCollection(self):
return False
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/method/delete_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/method/delete_common.py 2010-03-15 18:05:21 UTC (rev 5307)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/method/delete_common.py 2010-03-15 19:59:17 UTC (rev 5308)
@@ -215,6 +215,12 @@
# Now do normal delete
+ # Check virtual share first
+ isVirtual = yield delresource.isVirtualShare(self.request)
+ if isVirtual:
+ yield delresource.removeVirtualShare(self.request)
+ returnValue(responsecode.NO_CONTENT)
+
# Handle sharing
wasShared = (yield delresource.isShared(self.request))
if wasShared:
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/notifications.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/notifications.py 2010-03-15 18:05:21 UTC (rev 5307)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/notifications.py 2010-03-15 19:59:17 UTC (rev 5308)
@@ -73,8 +73,8 @@
def isCollection(self):
return True
- def resourceType(self):
- return davxml.ResourceType.notification
+ def resourceType(self, request):
+ return succeed(davxml.ResourceType.notification)
@inlineCallbacks
def addNotification(self, request, uid, xmltype, xmldata):
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/resource.py 2010-03-15 18:05:21 UTC (rev 5307)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/resource.py 2010-03-15 19:59:17 UTC (rev 5308)
@@ -266,6 +266,19 @@
"""
if qname == (u'DAV:', u'displayname'): return False # XXX HACK
if qname in self.liveProperties:
+ if qname in (
+ davxml.DisplayName.qname(),
+ customxml.Invite.qname(),
+ ):
+ return False
+ else:
+ return True
+ elif qname in (
+ customxml.GETCTag.qname(),
+ caldavxml.MaxResourceSize.qname(),
+ caldavxml.MaxAttendeesPerInstance.qname(),
+ caldavxml.ScheduleCalendarTransp.qname(),
+ ):
return True
else:
return False
@@ -291,8 +304,7 @@
elif (not self.isGlobalProperty(qname)) and isvirt:
ownerPrincipal = (yield self.resourceOwnerPrincipal(request))
p = self.deadProperties().get(qname, uid=ownerPrincipal.principalUID())
- if p is not None:
- returnValue(p)
+ returnValue(p)
res = (yield self._readGlobalProperty(qname, property, request))
returnValue(res)
@@ -469,8 +481,13 @@
# FIXME: Perhaps this is better done in authorize() instead.
@inlineCallbacks
def accessControlList(self, request, *args, **kwargs):
- acls = (yield super(CalDAVResource, self).accessControlList(request, *args, **kwargs))
+ isvirt = (yield self.isVirtualShare(request))
+ if isvirt:
+ acls = self.shareeAccessControlList()
+ else:
+ acls = (yield super(CalDAVResource, self).accessControlList(request, *args, **kwargs))
+
# Look for private events access classification
if self.hasDeadProperty(TwistedCalendarAccessProperty):
access = self.readDeadProperty(TwistedCalendarAccessProperty)
@@ -515,31 +532,39 @@
returnValue(acls)
+ @inlineCallbacks
def owner(self, request):
"""
Return the DAV:owner property value (MUST be a DAV:href or None).
"""
- def _gotParent(parent):
- if parent and isinstance(parent, CalDAVResource):
- return parent.owner(request)
+ isVirt = (yield self.isVirtualShare(request))
+ if isVirt:
+ parent = (yield self.locateParent(request, self._share.hosturl))
+ else:
+ parent = (yield self.locateParent(request, request.urlForResource(self)))
+ if parent and isinstance(parent, CalDAVResource):
+ result = (yield parent.owner(request))
+ returnValue(result)
+ else:
+ returnValue(None)
- d = self.locateParent(request, request.urlForResource(self))
- d.addCallback(_gotParent)
- return d
-
+ @inlineCallbacks
def ownerPrincipal(self, request):
"""
Return the DAV:owner property value (MUST be a DAV:href or None).
"""
- def _gotParent(parent):
- if parent and isinstance(parent, CalDAVResource):
- return parent.ownerPrincipal(request)
+ isVirt = (yield self.isVirtualShare(request))
+ if isVirt:
+ parent = (yield self.locateParent(request, self._share.hosturl))
+ else:
+ parent = (yield self.locateParent(request, request.urlForResource(self)))
+ if parent and isinstance(parent, CalDAVResource):
+ result = (yield parent.ownerPrincipal(request))
+ returnValue(result)
+ else:
+ returnValue(None)
- d = self.locateParent(request, request.urlForResource(self))
- d.addCallback(_gotParent)
- return d
-
def resourceOwnerPrincipal(self, request):
"""
This is the owner of the resource based on the URI used to access it. For a shared
@@ -1282,8 +1307,8 @@
self.parent = parent
- def resourceType(self):
- return davxml.ResourceType.searchaddressbook #@UndefinedVariable
+ def resourceType(self, request):
+ return succeed(davxml.ResourceType.searchaddressbook)
def renderHTTP(self, request):
return RedirectResponse(request.unparseURL(path="/directory/"))
@@ -1303,8 +1328,8 @@
self.parent = parent
- def resourceType(self):
- return davxml.ResourceType.searchalladdressbook #@UndefinedVariable
+ def resourceType(self, request):
+ return succeed(davxml.ResourceType.searchalladdressbook)
def renderHTTP(self, request):
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/schedule.py 2010-03-15 18:05:21 UTC (rev 5307)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/schedule.py 2010-03-15 19:59:17 UTC (rev 5308)
@@ -26,7 +26,7 @@
from twext.web2.dav.http import ErrorResponse
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
from twext.web2 import responsecode
from twext.web2.dav import davxml
from twext.web2.dav.util import joinURL, normalizeURL
@@ -87,8 +87,8 @@
(caldav_namespace, "schedule-default-calendar-URL"),
)
- def resourceType(self):
- return davxml.ResourceType.scheduleInbox
+ def resourceType(self, request):
+ return succeed(davxml.ResourceType.scheduleInbox)
def defaultAccessControlList(self):
@@ -253,8 +253,8 @@
else:
return super(ScheduleOutboxResource, self).defaultAccessControlList()
- def resourceType(self):
- return davxml.ResourceType.scheduleOutbox
+ def resourceType(self, request):
+ return succeed(davxml.ResourceType.scheduleOutbox)
@inlineCallbacks
def http_POST(self, request):
@@ -310,8 +310,8 @@
),
)
- def resourceType(self):
- return davxml.ResourceType.ischeduleinbox
+ def resourceType(self, request):
+ return succeed(davxml.ResourceType.ischeduleinbox)
def isCollection(self):
return False
Added: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharedcalendar.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharedcalendar.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharedcalendar.py 2010-03-15 19:59:17 UTC (rev 5308)
@@ -0,0 +1,81 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.python.log import LoggingMixIn
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+from twistedcaldav.extensions import DAVResource
+from twistedcaldav.resource import CalDAVComplianceMixIn
+from twistedcaldav.sharing import SharedCollectionMixin
+
+__all__ = [
+ "SharedCalendarResource",
+]
+
+"""
+Sharing behavior
+"""
+
+class SharedCalendarResource(CalDAVComplianceMixIn, SharedCollectionMixin, DAVResource, LoggingMixIn):
+
+ def __init__(self, parent, share):
+ self.parent = parent
+ self.share = share
+ super(SharedCalendarResource, self).__init__(self.parent.principalCollections())
+
+ @inlineCallbacks
+ def hostedResource(self, request):
+
+ if not hasattr(self, "_hostedResource"):
+ self._hostedResource = (yield request.locateResource(self.share.hosturl))
+ ownerPrincipal = (yield self.parent.ownerPrincipal(request))
+ self._hostedResource.setVirtualShare(ownerPrincipal, self.share)
+ returnValue(self._hostedResource)
+
+ def isCollection(self):
+ return True
+
+ @inlineCallbacks
+ def locateChild(self, request, segments):
+ hosted = (yield self.hostedResource(request))
+ result = (yield hosted.locateChild(request, segments))
+ returnValue(result)
+
+ def renderHTTP(self, request):
+ return self.hostedResource(request)
+
+ @inlineCallbacks
+ def getChild(self, name):
+ return self._hostedResource.getChild(name)
+
+ @inlineCallbacks
+ def hasProperty(self, property, request):
+ hosted = (yield self.hostedResource(request))
+ result = (yield hosted.hasProperty(property, request))
+ returnValue(result)
+
+ @inlineCallbacks
+ def readProperty(self, property, request):
+ hosted = (yield self.hostedResource(request))
+ result = (yield hosted.readProperty(property, request))
+ returnValue(result)
+
+ @inlineCallbacks
+ def writeProperty(self, property, request):
+ hosted = (yield self.hostedResource(request))
+ result = (yield hosted.writeProperty(property, request))
+ returnValue(result)
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharing.py 2010-03-15 18:05:21 UTC (rev 5307)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharing.py 2010-03-15 19:59:17 UTC (rev 5308)
@@ -13,29 +13,35 @@
# See the License for the specific language governing permissions and
# limitations under the License.
##
+from twext.web2.dav.resource import TwistedACLInheritable
+__all__ = [
+ "SharedCollectionMixin",
+]
+
from twext.python.log import LoggingMixIn
from twext.web2 import responsecode
+from twext.web2.dav import davxml
from twext.web2.dav.http import ErrorResponse, MultiStatusResponse
-from twext.web2.dav.util import allDataFromStream
+from twext.web2.dav.util import allDataFromStream, joinURL
from twext.web2.http import HTTPError, Response, StatusResponse
+
from twisted.internet.defer import succeed, inlineCallbacks, DeferredList,\
returnValue, fail
+
+from twistedcaldav import customxml, caldavxml
from twistedcaldav.config import config
+from twistedcaldav.extensions import updateCacheTokenOnCallback
from twistedcaldav.sql import AbstractSQLDatabase, db_prefix
+
from uuid import uuid4
+
from vobject.icalendar import dateTimeToString, utc
+
import datetime
import os
import types
-__all__ = [
- "SharedCollectionMixin",
-]
-
-from twistedcaldav import customxml
-from twext.web2.dav import davxml
-
"""
Sharing behavior
"""
@@ -61,6 +67,7 @@
return None
return self.isShared(request).addCallback(sharedOK)
+ @inlineCallbacks
def upgradeToShare(self, request):
""" Upgrade this collection to a shared state """
@@ -69,21 +76,21 @@
raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Cannot upgrade to shared calendar"))
# Change resourcetype
- rtype = self.resourceType()
+ rtype = (yield self.resourceType(request))
rtype = davxml.ResourceType(*(rtype.children + (customxml.SharedOwner(),)))
self.writeDeadProperty(rtype)
# Create invites database
self.invitesDB().create()
- return succeed(True)
+ returnValue(True)
@inlineCallbacks
def downgradeFromShare(self, request):
# Change resource type (note this might be called after deleting a resource
# so we have to cope with that)
- rtype = self.resourceType()
+ rtype = (yield self.resourceType(request))
rtype = davxml.ResourceType(*([child for child in rtype.children if child != customxml.SharedOwner()]))
self.writeDeadProperty(rtype)
@@ -133,10 +140,36 @@
""" Return True if this is an owner shared calendar collection """
return succeed(self.isSpecialCollection(customxml.SharedOwner))
+ def setVirtualShare(self, shareePrincipal, share):
+ self._isVirtualShare = True
+ self._shareePrincipal = shareePrincipal
+ self._share = share
+
def isVirtualShare(self, request):
""" Return True if this is a shared calendar collection """
- return succeed(self.isSpecialCollection(customxml.Shared))
+ return succeed(hasattr(self, "_isVirtualShare"))
+ def removeVirtualShare(self, request):
+ """ Return True if this is a shared calendar collection """
+
+ # Remove from sharee's calendar home
+ shareeHome = self._shareePrincipal.calendarHome()
+ return shareeHome.removeShare(request, self._share)
+
+ @inlineCallbacks
+ def resourceType(self, request):
+
+ rtype = (yield super(SharedCollectionMixin, self).resourceType(request))
+ isVirt = (yield self.isVirtualShare(request))
+ if isVirt:
+ rtype = davxml.ResourceType(
+ *(
+ tuple([child for child in rtype.children if child.qname() != customxml.SharedOwner.qname()]) +
+ (customxml.Shared(),)
+ )
+ )
+ returnValue(rtype)
+
def sharedResourceType(self):
"""
Return the DAV:resourcetype stripped of any shared elements.
@@ -149,6 +182,73 @@
else:
return ""
+ def shareeAccessControlList(self):
+
+ assert self._isVirtualShare, "Only call this fort a virtual share"
+
+ # Get the invite for this sharee
+ invite = self.invitesDB().recordForInviteUID(self._share.inviteuid)
+ if invite is None:
+ return davxml.ACL()
+
+ userprivs = [
+ ]
+ if invite.access in ("read-only", "read-write", "read-write-schedule",):
+ userprivs.append(davxml.Privilege(davxml.Read()))
+ userprivs.append(davxml.Privilege(davxml.ReadACL()))
+ userprivs.append(davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()))
+ if invite.access in ("read-write", "read-write-schedule",):
+ userprivs.append(davxml.Privilege(davxml.Write()))
+ proxyprivs = list(userprivs)
+ proxyprivs.remove(davxml.Privilege(davxml.ReadACL()))
+
+ aces = (
+ # Inheritable DAV:all access for the resource's associated principal.
+ davxml.ACE(
+ davxml.Principal(davxml.HRef(self._shareePrincipal.principalURL())),
+ davxml.Grant(*userprivs),
+ davxml.Protected(),
+ TwistedACLInheritable(),
+ ),
+ # Inheritable CALDAV:read-free-busy access for authenticated users.
+ davxml.ACE(
+ davxml.Principal(davxml.Authenticated()),
+ davxml.Grant(davxml.Privilege(caldavxml.ReadFreeBusy())),
+ TwistedACLInheritable(),
+ ),
+ )
+
+ # Give read access to config.ReadPrincipals
+ aces += config.ReadACEs
+
+ # Give all access to config.AdminPrincipals
+ aces += config.AdminACEs
+
+ if config.EnableProxyPrincipals:
+ aces += (
+ # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-read users.
+ davxml.ACE(
+ davxml.Principal(davxml.HRef(joinURL(self._shareePrincipal.principalURL(), "calendar-proxy-read/"))),
+ davxml.Grant(
+ davxml.Privilege(davxml.Read()),
+ davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
+ ),
+ davxml.Protected(),
+ TwistedACLInheritable(),
+ ),
+ # DAV:read/DAV:read-current-user-privilege-set/DAV:write access for this principal's calendar-proxy-write users.
+ davxml.ACE(
+ davxml.Principal(davxml.HRef(joinURL(self._shareePrincipal.principalURL(), "calendar-proxy-write/"))),
+ davxml.Grant(
+ davxml.Privilege(*proxyprivs),
+ ),
+ davxml.Protected(),
+ TwistedACLInheritable(),
+ ),
+ )
+
+ return davxml.ACL(*aces)
+
def validUserIDForShare(self, userid):
"""
Test the user id to see if it is a valid identifier for sharing and return a "normalized"
@@ -184,10 +284,6 @@
record.state = "INVALID"
self.invitesDB().addOrUpdateRecord(record)
- def removeVirtualShare(self, request):
- """ As user of a shared calendar, unlink this calendar collection """
- return succeed(False)
-
def getInviteUsers(self, request):
return succeed(True)
@@ -527,25 +623,203 @@
("text", "xml") : xmlPOSTAuth,
}
+inviteAccessMapToXML = {
+ "read-only" : customxml.ReadAccess,
+ "read-write" : customxml.ReadWriteAccess,
+ "read-write-schedule" : customxml.ReadWriteScheduleAccess,
+}
+inviteAccessMapFromXML = dict([(v,k) for k,v in inviteAccessMapToXML.iteritems()])
+
+inviteStatusMapToXML = {
+ "NEEDS-ACTION" : customxml.InviteStatusNoResponse,
+ "ACCEPTED" : customxml.InviteStatusAccepted,
+ "DECLINED" : customxml.InviteStatusDeclined,
+ "DELETED" : customxml.InviteStatusDeleted,
+ "INVALID" : customxml.InviteStatusInvalid,
+}
+inviteStatusMapFromXML = dict([(v,k) for k,v in inviteStatusMapToXML.iteritems()])
+
+class Invite(object):
+
+ def __init__(self, inviteuid, userid, access, state, summary):
+ self.inviteuid = inviteuid
+ self.userid = userid
+ self.access = access
+ self.state = state
+ self.summary = summary
+
+ def makePropertyElement(self):
+
+ return customxml.InviteUser(
+ customxml.UID.fromString(self.inviteuid),
+ davxml.HRef.fromString(self.userid),
+ customxml.InviteAccess(inviteAccessMapToXML[self.access]()),
+ inviteStatusMapToXML[self.state](),
+ )
+
+class InvitesDatabase(AbstractSQLDatabase, LoggingMixIn):
+
+ db_basename = db_prefix + "invites"
+ schema_version = "1"
+ db_type = "invites"
+
+ def __init__(self, resource):
+ """
+ @param resource: the L{twistedcaldav.static.CalDAVFile} resource for
+ the shared collection. C{resource} must be a calendar/addressbook collection.)
+ """
+ self.resource = resource
+ db_filename = os.path.join(self.resource.fp.path, InvitesDatabase.db_basename)
+ super(InvitesDatabase, self).__init__(db_filename, True, autocommit=True)
+
+ def create(self):
+ """
+ Create the index and initialize it.
+ """
+ self._db()
+
+ def allRecords(self):
+
+ records = self._db_execute("select * from INVITE order by USERID")
+ return [self._makeRecord(row) for row in (records if records is not None else ())]
+
+ def recordForUserID(self, userid):
+
+ row = self._db_execute("select * from INVITE where USERID = :1", userid)
+ return self._makeRecord(row[0]) if row else None
+
+ def recordForInviteUID(self, inviteUID):
+
+ row = self._db_execute("select * from INVITE where INVITEUID = :1", inviteUID)
+ return self._makeRecord(row[0]) if row else None
+
+ def addOrUpdateRecord(self, record):
+
+ self._db_execute("""insert or replace into INVITE (INVITEUID, USERID, ACCESS, STATE, SUMMARY)
+ values (:1, :2, :3, :4, :5)
+ """, record.inviteuid, record.userid, record.access, record.state, record.summary,
+ )
+
+ def removeRecordForUserID(self, userid):
+
+ self._db_execute("delete from INVITE where USERID = :1", userid)
+
+ def removeRecordForInviteUID(self, inviteUID):
+
+ self._db_execute("delete from INVITE where INVITEUID = :1", inviteUID)
+
+ def remove(self):
+
+ self._db_close()
+ os.remove(self.dbpath)
+
+ def _db_version(self):
+ """
+ @return: the schema version assigned to this index.
+ """
+ return InvitesDatabase.schema_version
+
+ def _db_type(self):
+ """
+ @return: the collection type assigned to this index.
+ """
+ return InvitesDatabase.db_type
+
+ def _db_init_data_tables(self, q):
+ """
+ Initialise the underlying database tables.
+ @param q: a database cursor to use.
+ """
+ #
+ # INVITE table is the primary table
+ # INVITEUID: UID for this invite
+ # NAME: identifier of invitee
+ # ACCESS: Access mode for share
+ # STATE: Invite response status
+ # SUMMARY: Invite summary
+ #
+ q.execute(
+ """
+ create table INVITE (
+ INVITEUID text unique,
+ USERID text unique,
+ ACCESS text,
+ STATE text,
+ SUMMARY text
+ )
+ """
+ )
+
+ q.execute(
+ """
+ create index USERID on INVITE (USERID)
+ """
+ )
+ q.execute(
+ """
+ create index INVITEUID on INVITE (INVITEUID)
+ """
+ )
+
+ def _db_upgrade_data_tables(self, q, old_version):
+ """
+ Upgrade the data from an older version of the DB.
+ """
+
+ # Nothing to do as we have not changed the schema
+ pass
+
+ def _makeRecord(self, row):
+
+ return Invite(*[str(item) if type(item) == types.UnicodeType else item for item in row])
+
class SharedHomeMixin(object):
"""
A mix-in for calendar/addressbook homes that defines the operations for manipulating a sharee's
set of shared calendfars.
"""
- def acceptShare(self, request, hostUrl, replytoUID, displayname=None):
- return self._changeShare(request, "ACCEPTED", hostUrl, replytoUID, displayname)
+ def sharesDB(self):
+
+ if not hasattr(self, "_sharesDB"):
+ self._sharesDB = SharedCalendarsDatabase(self)
+ return self._sharesDB
+ def provisionShares(self):
+
+ if not hasattr(self, "_provisionedShares"):
+ from twistedcaldav.sharedcalendar import SharedCalendarResource
+ for share in self.sharesDB().allRecords():
+ child = SharedCalendarResource(self, share)
+ self.putChild(share.localname, child)
+ self._provisionedShares = True
+
+ @updateCacheTokenOnCallback
+ def acceptShare(self, request, hostUrl, inviteUID, displayname=None):
+
+ # Add or update in DB
+ oldShare = self.sharesDB().recordForInviteUID(inviteUID)
+ if not oldShare:
+ share = SharedCalendarRecord(inviteUID, hostUrl, str(uuid4()), displayname)
+ self.sharesDB().addOrUpdateRecord(share)
+
+ return self._changeShare(request, "ACCEPTED", hostUrl, inviteUID, displayname)
+
def wouldAcceptShare(self, hostUrl, request):
return succeed(True)
- def removeShare(self, request, hostUrl, resourceName):
+ def removeShare(self, request, share):
""" Remove a shared calendar named in resourceName """
- return succeed(True)
+ return self.declineShare(request, share.hosturl, share.inviteuid)
- def declineShare(self, request, hostUrl, replytoUID):
- return self._changeShare(request, "DECLINED", hostUrl, replytoUID)
+ @updateCacheTokenOnCallback
+ def declineShare(self, request, hostUrl, inviteUID):
+ # Remove it if its in the DB
+ self.sharesDB().removeRecordForInviteUID(inviteUID)
+
+ return self._changeShare(request, "DECLINED", hostUrl, inviteUID)
+
@inlineCallbacks
def _changeShare(self, request, state, hostUrl, replytoUID, displayname=None):
""" Accept an invite to a shared calendar """
@@ -598,6 +872,7 @@
# Add to collections
yield notifications.addNotification(request, notificationUID, xmltype, xmldata)
+ @updateCacheTokenOnCallback
def xmlPOSTNoAuth(self, encoding, request):
def _handleResponse(result):
response = Response(code=responsecode.OK)
@@ -657,54 +932,28 @@
return allDataFromStream(request.stream).addCallback(_getData)
-inviteAccessMapToXML = {
- "read-only" : customxml.ReadAccess,
- "read-write" : customxml.ReadWriteAccess,
- "read-write-schedule" : customxml.ReadWriteScheduleAccess,
-}
-inviteAccessMapFromXML = dict([(v,k) for k,v in inviteAccessMapToXML.iteritems()])
-
-inviteStatusMapToXML = {
- "NEEDS-ACTION" : customxml.InviteStatusNoResponse,
- "ACCEPTED" : customxml.InviteStatusAccepted,
- "DECLINED" : customxml.InviteStatusDeclined,
- "DELETED" : customxml.InviteStatusDeleted,
- "INVALID" : customxml.InviteStatusInvalid,
-}
-inviteStatusMapFromXML = dict([(v,k) for k,v in inviteStatusMapToXML.iteritems()])
-
-class Invite(object):
+class SharedCalendarRecord(object):
- def __init__(self, inviteuid, userid, access, state, summary):
+ def __init__(self, inviteuid, hosturl, localname, summary):
self.inviteuid = inviteuid
- self.userid = userid
- self.access = access
- self.state = state
+ self.hosturl = hosturl
+ self.localname = localname
self.summary = summary
-
- def makePropertyElement(self):
-
- return customxml.InviteUser(
- customxml.UID.fromString(self.inviteuid),
- davxml.HRef.fromString(self.userid),
- customxml.InviteAccess(inviteAccessMapToXML[self.access]()),
- inviteStatusMapToXML[self.state](),
- )
-class InvitesDatabase(AbstractSQLDatabase, LoggingMixIn):
+class SharedCalendarsDatabase(AbstractSQLDatabase, LoggingMixIn):
- db_basename = db_prefix + "invites"
+ db_basename = db_prefix + "shares"
schema_version = "1"
- db_type = "invites"
+ db_type = "shares"
def __init__(self, resource):
"""
@param resource: the L{twistedcaldav.static.CalDAVFile} resource for
- the shared collection. C{resource} must be a calendar/addressbook collection.)
+ the shared collection. C{resource} must be a calendar/addressbook home collection.)
"""
self.resource = resource
- db_filename = os.path.join(self.resource.fp.path, InvitesDatabase.db_basename)
- super(InvitesDatabase, self).__init__(db_filename, True, autocommit=True)
+ db_filename = os.path.join(self.resource.fp.path, SharedCalendarsDatabase.db_basename)
+ super(SharedCalendarsDatabase, self).__init__(db_filename, True, autocommit=True)
def create(self):
"""
@@ -714,33 +963,33 @@
def allRecords(self):
- records = self._db_execute("select * from INVITE order by USERID")
+ records = self._db_execute("select * from SHARES order by LOCALNAME")
return [self._makeRecord(row) for row in (records if records is not None else ())]
- def recordForUserID(self, userid):
+ def recordForLocalName(self, localname):
- row = self._db_execute("select * from INVITE where USERID = :1", userid)
+ row = self._db_execute("select * from SHARES where LOCALNAME = :1", localname)
return self._makeRecord(row[0]) if row else None
def recordForInviteUID(self, inviteUID):
- row = self._db_execute("select * from INVITE where INVITEUID = :1", inviteUID)
+ row = self._db_execute("select * from SHARES where INVITEUID = :1", inviteUID)
return self._makeRecord(row[0]) if row else None
def addOrUpdateRecord(self, record):
- self._db_execute("""insert or replace into INVITE (INVITEUID, USERID, ACCESS, STATE, SUMMARY)
- values (:1, :2, :3, :4, :5)
- """, record.inviteuid, record.userid, record.access, record.state, record.summary,
+ 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,
)
- def removeRecordForUserID(self, userid):
+ def removeRecordForLocalName(self, localname):
- self._db_execute("delete from INVITE where USERID = :1", userid)
+ self._db_execute("delete from SHARES where LOCALNAME = :1", localname)
def removeRecordForInviteUID(self, inviteUID):
- self._db_execute("delete from INVITE where INVITEUID = :1", inviteUID)
+ self._db_execute("delete from SHARES where INVITEUID = :1", inviteUID)
def remove(self):
@@ -751,13 +1000,13 @@
"""
@return: the schema version assigned to this index.
"""
- return InvitesDatabase.schema_version
+ return SharedCalendarsDatabase.schema_version
def _db_type(self):
"""
@return: the collection type assigned to this index.
"""
- return InvitesDatabase.db_type
+ return SharedCalendarsDatabase.db_type
def _db_init_data_tables(self, q):
"""
@@ -765,20 +1014,18 @@
@param q: a database cursor to use.
"""
#
- # INVITE table is the primary table
+ # SHARES table is the primary table
# INVITEUID: UID for this invite
- # NAME: identifier of invitee
- # ACCESS: Access mode for share
- # STATE: Invite response status
+ # HOSTURL: URL for data source
+ # LOCALNAME: local path name
# SUMMARY: Invite summary
#
q.execute(
"""
- create table INVITE (
+ create table SHARES (
INVITEUID text unique,
- USERID text unique,
- ACCESS text,
- STATE text,
+ HOSTURL text,
+ LOCALNAME text,
SUMMARY text
)
"""
@@ -786,14 +1033,19 @@
q.execute(
"""
- create index USERID on INVITE (USERID)
+ create index INVITEUID on SHARES (INVITEUID)
"""
)
q.execute(
"""
- create index INVITEUID on INVITE (INVITEUID)
+ create index HOSTURL on SHARES (HOSTURL)
"""
)
+ q.execute(
+ """
+ create index LOCALNAME on SHARES (LOCALNAME)
+ """
+ )
def _db_upgrade_data_tables(self, q, old_version):
"""
@@ -805,5 +1057,4 @@
def _makeRecord(self, row):
- return Invite(*[str(item) if type(item) == types.UnicodeType else item for item in row])
-
+ return SharedCalendarRecord(*[str(item) if type(item) == types.UnicodeType else item for item in row])
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/static.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/static.py 2010-03-15 18:05:21 UTC (rev 5307)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/static.py 2010-03-15 19:59:17 UTC (rev 5308)
@@ -934,6 +934,12 @@
CalDAVFile.__init__(self, path)
DirectoryCalendarHomeResource.__init__(self, parent, record)
+ def provision(self):
+ result = super(CalendarHomeFile, self).provision()
+ if config.Sharing.Enabled:
+ self.provisionShares()
+ return result
+
def provisionChild(self, name):
if config.EnableDropBox:
DropBoxHomeFileClass = DropBoxHomeFile
@@ -963,7 +969,6 @@
child.cacheNotifier = self.cacheNotifier
child.clientNotifier = self.clientNotifier
return child
-
return self.createSimilarFile(self.fp.child(name).path)
def createSimilarFile(self, path):
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/test/test_sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/test/test_sharing.py 2010-03-15 18:05:21 UTC (rev 5307)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/test/test_sharing.py 2010-03-15 19:59:17 UTC (rev 5308)
@@ -81,13 +81,15 @@
def test_upgradeToShareOnCreate(self):
request = SimpleRequest(self.site, "MKCOL", "/calendar/")
- self.assertEquals(self.resource.resourceType(), davxml.ResourceType.calendar)
+ rtype = (yield self.resource.resourceType(request))
+ self.assertEquals(rtype, davxml.ResourceType.calendar)
propInvite = (yield self.resource.readProperty(customxml.Invite, request))
self.assertEquals(propInvite, None)
yield self.resource.upgradeToShare(request)
- self.assertEquals(self.resource.resourceType(), davxml.ResourceType.sharedcalendar)
+ rtype = (yield self.resource.resourceType(request))
+ self.assertEquals(rtype, davxml.ResourceType.sharedcalendar)
propInvite = (yield self.resource.readProperty(customxml.Invite, request))
self.assertEquals(propInvite, customxml.Invite())
@@ -100,13 +102,15 @@
def test_upgradeToShareAfterCreate(self):
request = SimpleRequest(self.site, "PROPPATCH", "/calendar/")
- self.assertEquals(self.resource.resourceType(), davxml.ResourceType.calendar)
+ rtype = (yield self.resource.resourceType(request))
+ self.assertEquals(rtype, davxml.ResourceType.calendar)
propInvite = (yield self.resource.readProperty(customxml.Invite, request))
self.assertEquals(propInvite, None)
self.assertRaises(HTTPError, self.resource.upgradeToShare, request)
- self.assertEquals(self.resource.resourceType(), davxml.ResourceType.calendar)
+ rtype = (yield self.resource.resourceType(request))
+ self.assertEquals(rtype, davxml.ResourceType.calendar)
propInvite = (yield self.resource.readProperty(customxml.Invite, request))
self.assertEquals(propInvite, None)
@@ -117,15 +121,19 @@
@inlineCallbacks
def test_downgradeFromShare(self):
+ request = SimpleRequest(self.site, "PROPPATCH", "/calendar/")
+
self.resource.writeDeadProperty(davxml.ResourceType.sharedcalendar)
self.resource.writeDeadProperty(customxml.Invite())
- self.assertEquals(self.resource.resourceType(), davxml.ResourceType.sharedcalendar)
+ rtype = (yield self.resource.resourceType(request))
+ self.assertEquals(rtype, davxml.ResourceType.sharedcalendar)
propInvite = (yield self.resource.readProperty(customxml.Invite, None))
self.assertEquals(propInvite, customxml.Invite())
yield self.resource.downgradeFromShare(None)
- self.assertEquals(self.resource.resourceType(), davxml.ResourceType.calendar)
+ rtype = (yield self.resource.resourceType(request))
+ self.assertEquals(rtype, davxml.ResourceType.calendar)
propInvite = (yield self.resource.readProperty(customxml.Invite, None))
self.assertEquals(propInvite, None)
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/timezoneservice.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/timezoneservice.py 2010-03-15 18:05:21 UTC (rev 5307)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/timezoneservice.py 2010-03-15 19:59:17 UTC (rev 5308)
@@ -28,10 +28,11 @@
from twext.web2.dav import davxml
from twext.web2.http import HTTPError
from twext.web2.http import Response
+from twext.web2.http import XMLResponse
from twext.web2.http_headers import MimeType
from twext.web2.stream import MemoryStream
-from twext.web2.http import XMLResponse
+from twisted.internet.defer import succeed
from twistedcaldav import customxml
from twistedcaldav.customxml import calendarserver_namespace
@@ -72,8 +73,8 @@
),
)
- def resourceType(self):
- return davxml.ResourceType.timezones
+ def resourceType(self, request):
+ return succeed(davxml.ResourceType.timezones)
def isCollection(self):
return False
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100315/51a23a89/attachment-0001.html>
More information about the calendarserver-changes
mailing list