[CalendarServer-changes] [3295] CalendarServer/trunk/twistedcaldav
source_changes at macosforge.org
source_changes at macosforge.org
Thu Oct 30 19:33:19 PDT 2008
Revision: 3295
http://trac.macosforge.org/projects/calendarserver/changeset/3295
Author: cdaboo at apple.com
Date: 2008-10-30 19:33:19 -0700 (Thu, 30 Oct 2008)
Log Message:
-----------
Major refactor to implement proper PUT/COPY/MOVE/DELETE logic with implicit scheduling and also
to restrict duplicate UIDs for scheduling messages.
Modified Paths:
--------------
CalendarServer/trunk/twistedcaldav/customxml.py
CalendarServer/trunk/twistedcaldav/method/delete.py
CalendarServer/trunk/twistedcaldav/method/put_common.py
CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py
Modified: CalendarServer/trunk/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/customxml.py 2008-10-31 02:20:00 UTC (rev 3294)
+++ CalendarServer/trunk/twistedcaldav/customxml.py 2008-10-31 02:33:19 UTC (rev 3295)
@@ -60,6 +60,14 @@
def getValue(self):
return str(self)
+class TwistedSchedulingObjectResource (davxml.WebDAVEmptyElement):
+ """
+ Indicates that the resource is a scheduling object resource.
+ """
+ namespace = twisted_private_namespace
+ name = "scheduling-object-resource"
+ hidden = True
+
class TwistedCalendarHasPrivateCommentsProperty (davxml.WebDAVEmptyElement):
"""
Indicates that a calendar resource has private comments.
Modified: CalendarServer/trunk/twistedcaldav/method/delete.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/delete.py 2008-10-31 02:20:00 UTC (rev 3294)
+++ CalendarServer/trunk/twistedcaldav/method/delete.py 2008-10-31 02:33:19 UTC (rev 3295)
@@ -22,13 +22,18 @@
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.fileop import delete
from twisted.web2.dav.util import parentForURL
from twisted.web2.http import HTTPError, StatusResponse
from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
from twistedcaldav.resource import isCalendarCollectionResource
from twistedcaldav.scheduling.implicit import ImplicitScheduler
+from twistedcaldav.log import Logger
+log = Logger()
+
@inlineCallbacks
def http_DELETE(self, request):
#
@@ -39,10 +44,28 @@
# TODO: need to use transaction based delete on live scheduling object resources
# as the iTIP operation may fail and may need to prevent the delete from happening.
+ if not self.fp.exists():
+ log.err("File not found: %s" % (self.fp.path,))
+ raise HTTPError(responsecode.NOT_FOUND)
+
+ depth = request.headers.getHeader("depth", "infinity")
+
+ #
+ # Check authentication and access controls
+ #
parentURL = parentForURL(request.uri)
parent = (yield request.locateResource(parentURL))
- calendar = None
+ yield parent.authorize(request, (davxml.Unbind(),))
+
+ # Do quota checks before we start deleting things
+ myquota = (yield self.quota(request))
+ if myquota is not None:
+ old_size = (yield self.quotaSize(request))
+ else:
+ old_size = 0
+
+ scheduler = None
isCalendarCollection = False
isCalendarResource = False
lock = None
@@ -51,7 +74,12 @@
if isCalendarCollectionResource(parent):
isCalendarResource = True
calendar = self.iCalendar()
- lock = MemcacheLock("ImplicitUIDLock", calendar.resourceUID(), timeout=60.0)
+ scheduler = ImplicitScheduler()
+ do_implicit_action, _ignore = (yield scheduler.testImplicitSchedulingDELETE(request, self, calendar))
+ if do_implicit_action:
+ lock = MemcacheLock("ImplicitUIDLock", calendar.resourceUID(), timeout=60.0)
+ else:
+ scheduler = None
elif isCalendarCollectionResource(self):
isCalendarCollection = True
@@ -60,8 +88,14 @@
if lock:
yield lock.acquire()
- response = (yield super(CalDAVFile, self).http_DELETE(request))
+ # Do delete
+ response = (yield delete(request.uri, self.fp, depth))
+
+ # Adjust quota
+ if myquota is not None:
+ yield self.quotaSizeAdjust(request, -old_size)
+
if response == responsecode.NO_CONTENT:
if isCalendarResource:
@@ -72,8 +106,8 @@
yield parent.updateCTag()
# Do scheduling
- scheduler = ImplicitScheduler()
- yield scheduler.doImplicitScheduling(request, self, calendar, True)
+ if scheduler:
+ yield scheduler.doImplicitScheduling()
elif isCalendarCollection:
Modified: CalendarServer/trunk/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_common.py 2008-10-31 02:20:00 UTC (rev 3294)
+++ CalendarServer/trunk/twistedcaldav/method/put_common.py 2008-10-31 02:33:19 UTC (rev 3295)
@@ -25,7 +25,7 @@
from twisted.internet import reactor
from twisted.internet.defer import Deferred, inlineCallbacks, succeed
-from twisted.internet.defer import maybeDeferred, returnValue
+from twisted.internet.defer import returnValue
from twisted.python import failure
from twisted.python.filepath import FilePath
from twisted.web2 import responsecode
@@ -48,9 +48,9 @@
from twistedcaldav.caldavxml import NumberOfRecurrencesWithinLimits
from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.customxml import calendarserver_namespace ,\
- TwistedCalendarHasPrivateCommentsProperty
+ TwistedCalendarHasPrivateCommentsProperty, TwistedSchedulingObjectResource
from twistedcaldav.customxml import TwistedCalendarAccessProperty
-from twistedcaldav.fileops import copyToWithXAttrs
+from twistedcaldav.fileops import copyToWithXAttrs, copyXAttrs
from twistedcaldav.fileops import putWithXAttrs
from twistedcaldav.fileops import copyWithXAttrs
from twistedcaldav.ical import Component, Property
@@ -265,11 +265,15 @@
self.rollback = None
self.access = None
+ @inlineCallbacks
def fullValidation(self):
"""
Do full validation of source and destination calendar data.
"""
+ # Basic validation
+ yield self.validCopyMoveOperation()
+
if self.destinationcal:
# Valid resource name check
result, message = self.validResourceName()
@@ -330,10 +334,38 @@
# Check access
if self.destinationcal and config.EnablePrivateEvents:
- return self.validAccess()
+ result = (yield self.validAccess())
+ returnValue(result)
else:
- return succeed(None)
+ returnValue(None)
+
+ elif self.sourcecal:
+ self.source_index = self.sourceparent.index()
+ self.calendar = self.source.iCalendar()
+ @inlineCallbacks
+ def validCopyMoveOperation(self):
+ """
+ Check that copy/move type behavior is valid.
+ """
+ if self.source:
+ if not self.destinationcal:
+ # Don't care about copies/moves to non-calendar destinations
+ # In theory this state should not occur here as COPY/MOVE won't call into this as
+ # they detect this state and do regular WebDAV copy/move.
+ pass
+ elif not self.sourcecal:
+ # Moving into a calendar requires regular checks
+ pass
+ else:
+ # Calendar to calendar moves are OK if the owner is the same
+ sourceowner = (yield self.sourceparent.owner(self.request))
+ destowner = (yield self.destinationparent.owner(self.request))
+ if sourceowner != destowner:
+ msg = "Calendar-to-calendar %s with different owners are not supported" % ("moves" if self.deletesource else "copies",)
+ log.debug(msg)
+ raise HTTPError(StatusResponse(responsecode.FORBIDDEN, msg))
+
def validResourceName(self):
"""
Make sure that the resource name for the new resource is valid.
@@ -546,29 +578,99 @@
copyToWithXAttrs(self.source.fp, self.rollback.source_copy)
log.debug("Rollback: backing up source %s to %s" % (self.source.fp.path, self.rollback.source_copy.path))
+ def preservePrivateComments(self):
+ # Check for private comments on the old resource and the new resource and re-insert
+ # ones that are lost.
+ #
+ # NB Do this before implicit scheduling as we don't want old clients to trigger scheduling when
+ # the X- property is missing.
+ new_has_private_comments = False
+ if config.Scheduling["CalDAV"].get("EnablePrivateComments", True) and self.calendar is not None:
+ old_has_private_comments = self.destination.exists() and self.destinationcal and self.destination.hasDeadProperty(TwistedCalendarHasPrivateCommentsProperty)
+ new_has_private_comments = self.calendar.hasPropertyInAnyComponent((
+ "X-CALENDARSERVER-PRIVATE-COMMENT",
+ "X-CALENDARSERVER-ATTENDEE-COMMENT",
+ ))
+
+ if old_has_private_comments and not new_has_private_comments:
+ # Transfer old comments to new calendar
+ log.debug("Private Comments properties were entirely removed by the client. Restoring existing properties.")
+ old_calendar = self.destination.iCalendar()
+ self.calendar.transferProperties(old_calendar, (
+ "X-CALENDARSERVER-PRIVATE-COMMENT",
+ "X-CALENDARSERVER-ATTENDEE-COMMENT",
+ ))
+ self.calendardata = None
+
+ return new_has_private_comments
+
@inlineCallbacks
- def doStore(self):
- # Do put or copy based on whether source exists
- if self.source is not None:
- response = maybeDeferred(copyWithXAttrs, self.source.fp, self.destination.fp, self.destination_uri)
+ def doImplicitScheduling(self):
+ data_changed = False
+
+ # Do scheduling
+ if not self.isiTIP:
+ scheduler = ImplicitScheduler()
+
+ # Determine type of operation PUT, COPY or DELETE
+ if not self.source:
+ # PUT
+ do_implicit_action, is_scheduling_resource = (yield scheduler.testImplicitSchedulingPUT(
+ self.request,
+ self.destination,
+ self.destination_uri,
+ self.calendar,
+ internal_request=self.internal_request,
+ ))
+ elif self.deletesource:
+ # MOVE
+ do_implicit_action, is_scheduling_resource = (yield scheduler.testImplicitSchedulingMOVE(
+ self.request,
+ self.source,
+ self.sourcecal,
+ self.source_uri,
+ self.destination,
+ self.destinationcal,
+ self.destination_uri,
+ self.calendar,
+ internal_request=self.internal_request,
+ ))
+ else:
+ # COPY
+ do_implicit_action, is_scheduling_resource = (yield scheduler.testImplicitSchedulingCOPY(
+ self.request,
+ self.source,
+ self.sourcecal,
+ self.source_uri,
+ self.destination,
+ self.destinationcal,
+ self.destination_uri,
+ self.calendar,
+ internal_request=self.internal_request,
+ ))
+
+ if do_implicit_action and self.allowImplicitSchedule:
+ new_calendar = (yield scheduler.doImplicitScheduling())
+ if new_calendar:
+ self.calendar = new_calendar
+ self.calendardata = str(self.calendar)
+ data_changed = True
else:
- if self.calendardata is None:
- self.calendardata = str(self.calendar)
- md5 = MD5StreamWrapper(MemoryStream(self.calendardata))
- response = maybeDeferred(putWithXAttrs, md5, self.destination.fp)
- response = (yield response)
+ is_scheduling_resource = False
+
+ returnValue((is_scheduling_resource, data_changed,))
- # Update the MD5 value on the resource
+ @inlineCallbacks
+ def doStore(self, implicit):
+ # Do put or copy based on whether source exists
if self.source is not None:
- # Copy MD5 value from source to destination
- if self.source.hasDeadProperty(TwistedGETContentMD5):
- md5 = self.source.readDeadProperty(TwistedGETContentMD5)
- self.destination.writeDeadProperty(md5)
+ if implicit:
+ response = (yield self.doStorePut())
+ copyXAttrs(self.source.fp, self.destination.fp)
+ else:
+ response = (yield copyWithXAttrs(self.source.fp, self.destination.fp, self.destination_uri))
else:
- # Finish MD5 calculation and write dead property
- md5.close()
- md5 = md5.getMD5()
- self.destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
+ response = (yield self.doStorePut())
# Update calendar-access property value on the resource
if self.access:
@@ -583,6 +685,21 @@
returnValue(IResponse(response))
@inlineCallbacks
+ def doStorePut(self):
+
+ if self.calendardata is None:
+ self.calendardata = str(self.calendar)
+ md5 = MD5StreamWrapper(MemoryStream(self.calendardata))
+ response = (yield putWithXAttrs(md5, self.destination.fp))
+
+ # Finish MD5 calculation and write dead property
+ md5.close()
+ md5 = md5.getMD5()
+ self.destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
+
+ returnValue(response)
+
+ @inlineCallbacks
def doSourceDelete(self):
# Delete index for original item
if self.sourcecal:
@@ -715,35 +832,11 @@
# Get current quota state.
yield self.checkQuota()
- # Check for private comments on the old resource and the new resource and re-insert
- # ones that are lost.
- #
- # NB Do this before implicit scheduling as we don't want old clients to trigger scheduling when
- # the X- property is missing.
- if config.Scheduling["CalDAV"].get("EnablePrivateComments", True):
- old_has_private_comments = self.destination.exists() and self.destinationcal and self.destination.hasDeadProperty(TwistedCalendarHasPrivateCommentsProperty)
- new_has_private_comments = self.calendar.hasPropertyInAnyComponent((
- "X-CALENDARSERVER-PRIVATE-COMMENT",
- "X-CALENDARSERVER-ATTENDEE-COMMENT",
- ))
-
- if old_has_private_comments and not new_has_private_comments:
- # Transfer old comments to new calendar
- log.debug("Private Comments properties were entirely removed by the client. Restoring existing properties.")
- old_calendar = self.destination.iCalendar()
- self.calendar.transferProperties(old_calendar, (
- "X-CALENDARSERVER-PRIVATE-COMMENT",
- "X-CALENDARSERVER-ATTENDEE-COMMENT",
- ))
- self.calendardata = None
+ # Preserve private comments
+ new_has_private_comments = self.preservePrivateComments()
# Do scheduling
- if not self.isiTIP and self.allowImplicitSchedule:
- scheduler = ImplicitScheduler()
- new_calendar = (yield scheduler.doImplicitScheduling(self.request, self.destination, self.calendar, False, internal_request=self.internal_request))
- if new_calendar:
- self.calendar = new_calendar
- self.calendardata = str(self.calendar)
+ is_scheduling_resource, data_changed = (yield self.doImplicitScheduling())
# Initialize the rollback system
self.setupRollback()
@@ -763,9 +856,15 @@
"""
# Do the actual put or copy
- response = (yield self.doStore())
+ response = (yield self.doStore(data_changed))
+ # Check for scheduling object resource and write property
+ if is_scheduling_resource:
+ self.destination.writeDeadProperty(TwistedSchedulingObjectResource())
+ elif not self.destinationcal:
+ self.destination.removeDeadProperty(TwistedSchedulingObjectResource)
+
# Check for existence of private comments and write property
if config.Scheduling["CalDAV"].get("EnablePrivateComments", True):
if new_has_private_comments:
Modified: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2008-10-31 02:20:00 UTC (rev 3294)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2008-10-31 02:33:19 UTC (rev 3295)
@@ -17,7 +17,10 @@
from twisted.internet.defer import inlineCallbacks, returnValue, succeed
from twisted.web2 import responsecode
from twisted.web2.dav.http import ErrorResponse
+from twisted.web2.dav.util import joinURL
+from twisted.web2.dav.util import parentForURL
from twisted.web2.http import HTTPError
+
from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.scheduling.itip import iTipGenerator
from twistedcaldav.log import Logger
@@ -29,6 +32,7 @@
from twistedcaldav.scheduling import addressmapping
from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser,\
LocalCalendarUser
+from twistedcaldav.customxml import TwistedSchedulingObjectResource
__all__ = [
"ImplicitScheduler",
@@ -51,48 +55,154 @@
pass
@inlineCallbacks
- def doImplicitScheduling(self, request, resource, calendar, deleting, internal_request=False):
- """
- Do implicit scheduling operation based on the calendar data that is being PUT
+ def testImplicitSchedulingPUT(self, request, resource, resource_uri, calendar, internal_request=False):
+
+ self.request = request
+ self.resource = resource
+ self.calendar = calendar
+ self.internal_request = internal_request
- @param request:
- @type request:
- @param resource:
- @type resource:
- @param calendar: the calendar data being written, or None if deleting
- @type calendar: L{Component} or C{None}
- @param deleting: C{True} if the resource is being deleting
- @type deleting: bool
+ existing_resource = resource.exists()
+ existing_type = "schedule" if existing_resource and resource.hasDeadProperty(TwistedSchedulingObjectResource()) else "calendar"
+ new_type = "schedule" if (yield self.checkImplicitState()) else "calendar"
- @return: a new calendar object modified with scheduling information,
- or C{None} if nothing happened
- """
+ if existing_type == "calendar":
+ self.action = "create" if new_type == "schedule" else "none"
+ else:
+ self.action = "modify" if new_type == "schedule" else "remove"
+
+ # Cannot create new resource with existing UID
+ if not existing_resource or self.action == "create":
+ yield self.hasCalendarResourceUIDSomewhereElse(None, resource_uri, new_type)
+
+ # If action is remove we actually need to get state from the existing scheduling object resource
+ if self.action == "remove":
+ # Also make sure that we return the new calendar being be written rather than the old one
+ # when the implicit action is executed
+ self.return_calendar = calendar
+ self.calendar = resource.iCalendar()
+ yield self.checkImplicitState()
+ # Attendees are not allowed to overwrite one type with another
+ if self.state == "attendee" and (existing_type != new_type) and existing_resource:
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-attendee-change")))
+
+ returnValue((self.action != "none", new_type == "schedule",))
+
+ @inlineCallbacks
+ def testImplicitSchedulingMOVE(self, request, srcresource, srccal, src_uri, destresource, destcal, dest_uri, calendar, internal_request=False):
+
self.request = request
+ self.resource = destresource
+ self.calendar = calendar
+ self.internal_request = internal_request
+
+ new_type = "schedule" if (yield self.checkImplicitState()) else "calendar"
+
+ dest_exists = destresource.exists()
+ dest_is_implicit = destresource.hasDeadProperty(TwistedSchedulingObjectResource()) if dest_exists else False
+ src_is_implicit = srcresource.hasDeadProperty(TwistedSchedulingObjectResource()) or new_type == "schedule"
+
+ if srccal and destcal:
+ if src_is_implicit and dest_exists or dest_is_implicit:
+ log.debug("Implicit - cannot MOVE with a scheduling object resource")
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "unique-scheduling-object-resource")))
+ else:
+ self.action = "none"
+ elif srccal and not destcal:
+ result = (yield self.testImplicitSchedulingDELETE(request, srcresource, calendar))
+ returnValue((result[0], new_type == "schedule",))
+ elif not srccal and destcal:
+ result = (yield self.testImplicitSchedulingPUT(request, destresource, dest_uri, calendar))
+ returnValue(result)
+ else:
+ self.action = "none"
+
+ returnValue((self.action != "none", new_type == "schedule",))
+
+ @inlineCallbacks
+ def testImplicitSchedulingCOPY(self, request, srcresource, srccal, src_uri, destresource, destcal, dest_uri, calendar, internal_request=False):
+
+ self.request = request
+ self.resource = destresource
+ self.calendar = calendar
+ self.internal_request = internal_request
+
+ new_type = "schedule" if (yield self.checkImplicitState()) else "calendar"
+
+ dest_exists = destresource.exists()
+ dest_is_implicit = destresource.hasDeadProperty(TwistedSchedulingObjectResource()) if dest_exists else False
+ src_is_implicit = srcresource.hasDeadProperty(TwistedSchedulingObjectResource()) or new_type == "schedule"
+
+ if srccal and destcal:
+ if src_is_implicit or dest_is_implicit:
+ log.debug("Implicit - cannot COPY with a scheduling object resource")
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "unique-scheduling-object-resource")))
+ else:
+ self.action = "none"
+ elif srccal and not destcal:
+ self.action = "none"
+ elif not srccal and destcal:
+ result = (yield self.testImplicitSchedulingPUT(request, destresource, dest_uri, calendar))
+ returnValue(result)
+ else:
+ self.action = "none"
+
+ returnValue((self.action != "none", src_is_implicit,))
+
+ @inlineCallbacks
+ def testImplicitSchedulingDELETE(self, request, resource, calendar, internal_request=False):
+
+ self.request = request
self.resource = resource
self.calendar = calendar
- self.calendar_owner = (yield self.resource.owner(self.request))
- self.deleting = deleting
self.internal_request = internal_request
- self.except_attendees = ()
- # When deleting we MUST have the calendar as the actual resource
- # will have been deleted by now
- assert deleting and calendar or not deleting
+ yield self.checkImplicitState()
+ resource_type = "schedule" if resource.hasDeadProperty(TwistedSchedulingObjectResource()) else "calendar"
+ self.action = "remove" if resource_type == "schedule" else "none"
+
+ returnValue((self.action != "none", False,))
+
+ @inlineCallbacks
+ def checkImplicitState(self):
# Get some useful information from the calendar
yield self.extractCalendarData()
+ self.calendar_owner = (yield self.resource.owner(self.request))
# Determine what type of scheduling this is: Organizer triggered or Attendee triggered
organizer_scheduling = (yield self.isOrganizerScheduling())
if organizer_scheduling:
+ self.state = "organizer"
+ elif self.isAttendeeScheduling():
+ self.state = "attendee"
+ else:
+ self.state = None
+
+ returnValue(self.state is not None)
+
+ @inlineCallbacks
+ def doImplicitScheduling(self):
+ """
+ Do implicit scheduling operation based on the data already set by call to checkImplicitScheduling.
+
+ @return: a new calendar object modified with scheduling information,
+ or C{None} if nothing happened
+ """
+
+ # Setup some parameters
+ self.except_attendees = ()
+
+ # Determine what type of scheduling this is: Organizer triggered or Attendee triggered
+ if self.state == "organizer":
yield self.doImplicitOrganizer()
- elif self.isAttendeeScheduling():
+ elif self.state == "attendee":
yield self.doImplicitAttendee()
else:
returnValue(None)
- returnValue(self.calendar)
+ returnValue(self.return_calendar if hasattr(self, "return_calendar") else self.calendar)
@inlineCallbacks
def refreshAllAttendeesExceptSome(self, request, resource, calendar, attendees):
@@ -109,8 +219,10 @@
self.request = request
self.resource = resource
self.calendar = calendar
+ self.state = "organizer"
+ self.action = "modify"
+
self.calendar_owner = None
- self.deleting = False
self.internal_request = True
self.except_attendees = attendees
self.changed_rids = None
@@ -134,8 +246,10 @@
self.request = request
self.resource = resource
self.calendar = calendar
+ self.action = "modify"
+ self.state = "attendee"
+
self.calendar_owner = None
- self.deleting = False
self.internal_request = True
self.changed_rids = None
@@ -191,6 +305,53 @@
self.uid = self.calendar.resourceUID()
@inlineCallbacks
+ def hasCalendarResourceUIDSomewhereElse(self, src_uri, dest_uri, type):
+ """
+ See if a calendar component with a matching UID exists anywhere in the calendar home of the
+ current recipient owner and is not the resource being targeted.
+ """
+
+ # Don't care in some cases
+ if self.internal_request or self.action == "remove":
+ returnValue(None)
+
+ # Get owner's calendar-home
+ calendar_owner_principal = (yield self.resource.ownerPrincipal(self.request))
+ calendar_home = calendar_owner_principal.calendarHome()
+
+ source_parent_uri = parentForURL(src_uri)[:-1] if src_uri else None
+ destination_parent_uri = parentForURL(dest_uri)[:-1] if dest_uri else None
+
+ # FIXME: because of the URL->resource request mapping thing, we have to force the request
+ # to recognize this resource
+ self.request._rememberResource(calendar_home, calendar_home.url())
+
+ # Run a UID query against the UID
+
+ @inlineCallbacks
+ def queryCalendarCollection(collection, uri):
+ rname = collection.index().resourceNameForUID(self.uid)
+ if rname:
+ child = (yield self.request.locateResource(joinURL(uri, rname)))
+ matched_type = "schedule" if child and child.hasDeadProperty(TwistedSchedulingObjectResource()) else "calendar"
+ if (
+ uri != destination_parent_uri and
+ (source_parent_uri is None or uri != source_parent_uri) and
+ (type == "schedule" or matched_type == "schedule")
+ ):
+ log.debug("Implicit - found component with same UID in a different collection: %s" % (uri,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "unique-scheduling-object-resource")))
+
+ # Here we can always return true as the unique UID in a calendar collection
+ # requirement will already have been tested.
+
+ returnValue(True)
+
+ # NB We are by-passing privilege checking here. That should be OK as the data found is not
+ # exposed to the user.
+ yield report_common.applyToCalendarCollections(calendar_home, self.request, calendar_home.url(), "infinity", queryCalendarCollection, None)
+
+ @inlineCallbacks
def isOrganizerScheduling(self):
"""
Test whether this is a scheduling operation by an organizer
@@ -253,16 +414,16 @@
yield self.doAccessControl(self.organizerPrincipal, True)
# Check for a delete
- if self.deleting:
+ if self.action == "remove":
- log.debug("Implicit - organizer '%s' is deleting UID: '%s'" % (self.organizer, self.uid))
+ log.debug("Implicit - organizer '%s' is removing UID: '%s'" % (self.organizer, self.uid))
self.oldcalendar = self.calendar
# Cancel all attendees
self.cancelledAttendees = [(attendee, None) for attendee in self.attendees]
# Check for a new resource or an update
- elif self.resource.exists():
+ elif self.action == "modify":
# Read in existing data
self.oldcalendar = self.resource.iCalendar()
@@ -271,14 +432,15 @@
no_change, self.changed_rids = self.isChangeInsignificant()
if no_change:
# Nothing to do
- log.debug("Implicit - organizer '%s' is updating UID: '%s' but change is not significant" % (self.organizer, self.uid))
+ log.debug("Implicit - organizer '%s' is modifying UID: '%s' but change is not significant" % (self.organizer, self.uid))
returnValue(None)
- log.debug("Implicit - organizer '%s' is updating UID: '%s'" % (self.organizer, self.uid))
+ log.debug("Implicit - organizer '%s' is modifying UID: '%s'" % (self.organizer, self.uid))
# Check for removed attendees
self.findRemovedAttendees()
- else:
+
+ elif self.action == "create":
log.debug("Implicit - organizer '%s' is creating UID: '%s'" % (self.organizer, self.uid))
self.oldcalendar = None
self.changed_rids = None
@@ -377,7 +539,7 @@
yield self.processCancels()
# Process regular requests next
- if not self.deleting:
+ if self.action in ("create", "modify",):
yield self.processRequests()
@inlineCallbacks
@@ -402,7 +564,7 @@
if None in rids:
# One big CANCEL will do
- itipmsg = iTipGenerator.generateCancel(self.oldcalendar, (attendee,), None, self.deleting)
+ itipmsg = iTipGenerator.generateCancel(self.oldcalendar, (attendee,), None, self.action == "remove")
else:
# Multiple CANCELs
itipmsg = iTipGenerator.generateCancel(self.oldcalendar, (attendee,), rids)
@@ -470,9 +632,7 @@
if not self.internal_request:
yield self.doAccessControl(self.attendeePrincipal, False)
- if self.deleting:
- #log.error("Attendee '%s' is not allowed to delete an organized event: UID:%s" % (self.attendeePrincipal, self.uid,))
- #raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-attendee-change")))
+ if self.action == "remove":
log.debug("Implicit - attendee '%s' is cancelling UID: '%s'" % (self.attendee, self.uid))
yield self.scheduleCancelWithOrganizer()
Modified: CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py 2008-10-31 02:20:00 UTC (rev 3294)
+++ CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py 2008-10-31 02:33:19 UTC (rev 3295)
@@ -13,8 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
##
-from twistedcaldav.memcachelock import MemcacheLock
-from twistedcaldav.memcacher import Memcacher
import os
@@ -26,6 +24,9 @@
from twisted.web2.test.test_server import SimpleRequest
from twistedcaldav.ical import Component
+from twistedcaldav.memcachelock import MemcacheLock
+from twistedcaldav.memcacher import Memcacher
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
import twistedcaldav.test.util
class CollectionContents (twistedcaldav.test.util.TestCase):
@@ -35,6 +36,8 @@
data_dir = os.path.join(os.path.dirname(__file__), "data")
def setUp(self):
+
+ # Need to fake out memcache
def _getFakeMemcacheProtocol(self):
result = super(MemcacheLock, self)._getMemcacheProtocol()
@@ -45,6 +48,12 @@
MemcacheLock._getMemcacheProtocol = _getFakeMemcacheProtocol
+ # Need to not do implicit behavior during these tests
+ def _fakeDoImplicitScheduling(self):
+ return False, False
+
+ StoreCalendarObjectResource.doImplicitScheduling = _fakeDoImplicitScheduling
+
super(CollectionContents, self).setUp()
def test_collection_in_calendar(self):
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20081030/180e4c77/attachment-0001.html>
More information about the calendarserver-changes
mailing list