[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