[CalendarServer-changes] [10973] CalendarServer/trunk/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Tue Apr 2 11:59:24 PDT 2013


Revision: 10973
          http://trac.calendarserver.org//changeset/10973
Author:   cdaboo at apple.com
Date:     2013-04-02 11:59:24 -0700 (Tue, 02 Apr 2013)
Log Message:
-----------
Add X- property is for unscheduled events created on location or resource calendars. Prevent unscheduled events on
location or resource calendars if so configured.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/method/put_common.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/twistedcaldav/test/test_validation.py

Modified: CalendarServer/trunk/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_common.py	2013-04-02 18:56:24 UTC (rev 10972)
+++ CalendarServer/trunk/twistedcaldav/method/put_common.py	2013-04-02 18:59:24 UTC (rev 10973)
@@ -203,6 +203,7 @@
         self.access = None
         self.hasPrivateComments = False
         self.isScheduleResource = False
+        self.dataChanged = False
 
 
     @inlineCallbacks
@@ -365,6 +366,9 @@
             # Check that moves to shared calendars are OK
             yield self.validCopyMoveOperation()
 
+            # Check location/resource organizer requirement
+            yield self.validLocationResourceOrganizer()
+
             # Check access
             if self.destinationcal and config.EnablePrivateEvents:
                 result = (yield self.validAccess())
@@ -662,6 +666,53 @@
 
 
     @inlineCallbacks
+    def validLocationResourceOrganizer(self):
+        """
+        If the calendar owner is a location or resource, check whether an ORGANIZER property is required.
+        """
+
+        if not self.internal_request:
+            originatorPrincipal = (yield self.destination.ownerPrincipal(self.request))
+            cutype = originatorPrincipal.getCUType() if originatorPrincipal is not None else "INDIVIDUAL"
+            organizer = self.calendar.getOrganizer()
+
+            # Check for an allowed change
+            if organizer is None and (
+                cutype == "ROOM" and not config.Scheduling.Options.AllowLocationWithoutOrganizer or
+                cutype == "RESOURCE" and not config.Scheduling.Options.AllowResourceWithoutOrganizer):
+                raise HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    (calendarserver_namespace, "valid-organizer"),
+                    "Organizer required in calendar data",
+                ))
+
+            # Check for tracking the modifier
+            if organizer is None and (
+                cutype == "ROOM" and config.Scheduling.Options.TrackUnscheduledLocationData or
+                cutype == "RESOURCE" and config.Scheduling.Options.TrackUnscheduledResourceData):
+
+                # Find current principal
+                authz = None
+                authz_principal = self.destinationparent.currentPrincipal(self.request).children[0]
+                if isinstance(authz_principal, davxml.HRef):
+                    principalURL = str(authz_principal)
+                    if principalURL:
+                        authz = (yield self.request.locateResource(principalURL))
+
+                if authz is not None:
+                    prop = Property("X-CALENDARSERVER-MODIFIED-BY", "urn:uuid:%s" % (authz.record.guid,))
+                    prop.setParameter("CN", authz.displayName())
+                    for candidate in authz.calendarUserAddresses():
+                        if candidate.startswith("mailto:"):
+                            prop.setParameter("EMAIL", candidate)
+                            break
+                    self.calendar.replacePropertyInAllComponents(prop)
+                else:
+                    self.calendar.removeAllPropertiesWithName("X-CALENDARSERVER-MODIFIED-BY")
+                self.dataChanged = True
+
+
+    @inlineCallbacks
     def preservePrivateComments(self):
         # Check for private comments on the old resource and the new resource and re-insert
         # ones that are lost.
@@ -769,7 +820,6 @@
         """
 
         # Only relevant if calendar is sharee collection
-        changed = False
         if self.destinationparent.isShareeCollection():
 
             # Get all X-APPLE-DROPBOX's and ATTACH's that are http URIs
@@ -813,59 +863,52 @@
                     uri = uriNormalize(xdropbox.value())
                     if uri:
                         xdropbox.setValue(uri)
-                        changed = True
+                        self.dataChanged = True
                 for attachment in attachments:
                     uri = uriNormalize(attachment.value())
                     if uri:
                         attachment.setValue(uri)
-                        changed = True
+                        self.dataChanged = True
 
-        returnValue(changed)
 
-
     def processAlarms(self):
         """
         Remove duplicate alarms. Add a default alarm if required.
-
-        @return: indicate whether a change was made
-        @rtype: C{bool}
         """
 
         # Remove duplicate alarms
-        changed = False
-        if config.RemoveDuplicateAlarms:
-            changed = self.calendar.hasDuplicateAlarms(doFix=True)
+        if config.RemoveDuplicateAlarms and self.calendar.hasDuplicateAlarms(doFix=True):
+            self.dataChanged = True
 
         # Only if feature enabled
         if not config.EnableDefaultAlarms:
-            return changed
+            return
 
         # Check that we are creating and this is not the inbox
         if not self.destinationcal or self.destination.exists() or self.isiTIP:
-            return changed
+            return
 
         # Never add default alarms to calendar data in shared calendars
         if self.destinationparent.isShareeCollection():
-            return changed
+            return
 
         # Add default alarm for VEVENT and VTODO only
         mtype = self.calendar.mainType().upper()
         if self.calendar.mainType().upper() not in ("VEVENT", "VTODO"):
-            return changed
+            return
         vevent = mtype == "VEVENT"
 
         # Check timed or all-day
         start, _ignore_end = self.calendar.mainComponent(allow_multiple=True).getEffectiveStartEnd()
         if start is None:
             # Yes VTODOs might have no DTSTART or DUE - in this case we do not add a default
-            return changed
+            return
         timed = not start.isDateOnly()
 
         # See if default exists and add using appropriate logic
         alarm = self.destinationparent.getDefaultAlarm(vevent, timed)
-        if alarm:
-            changed = self.calendar.addAlarms(alarm)
-        return changed
+        if alarm and self.calendar.addAlarms(alarm):
+            self.dataChanged = True
 
 
     @inlineCallbacks
@@ -1221,14 +1264,14 @@
             yield self.replaceMissingToDoProperties()
 
             # Handle sharing dropbox normalization
-            dropboxChanged = (yield self.dropboxPathNormalization())
+            yield self.dropboxPathNormalization()
 
             # Pre-process managed attachments
             if not self.internal_request and not self.attachmentProcessingDone:
                 managed_copied, managed_removed = (yield self.destination.preProcessManagedAttachments(self.calendar))
 
             # Default/duplicate alarms
-            alarmChanged = self.processAlarms()
+            self.processAlarms()
 
             # Do scheduling
             implicit_result = (yield self.doImplicitScheduling())
@@ -1276,7 +1319,7 @@
                 yield self.destination.postProcessManagedAttachments(managed_copied, managed_removed)
 
             # Must not set ETag in response if data changed
-            if did_implicit_action or dropboxChanged or alarmChanged:
+            if did_implicit_action or self.dataChanged:
                 def _removeEtag(request, response):
                     response.headers.removeHeader('etag')
                     return response

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2013-04-02 18:56:24 UTC (rev 10972)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2013-04-02 18:59:24 UTC (rev 10973)
@@ -694,6 +694,10 @@
             "AllowGroupAsOrganizer"               : False, # Allow groups to be Organizers
             "AllowLocationAsOrganizer"            : False, # Allow locations to be Organizers
             "AllowResourceAsOrganizer"            : False, # Allow resources to be Organizers
+            "AllowLocationWithoutOrganizer"       : True, # Allow locations to have events without an Organizer
+            "AllowResourceWithoutOrganizer"       : True, # Allow resources to have events without an Organizer
+            "TrackUnscheduledLocationData"        : True, # Track who the last modifier of an unscheduled location event is
+            "TrackUnscheduledResourceData"        : True, # Track who the last modifier of an unscheduled resource event is
             "LimitFreeBusyAttendees"              : 30, # Maximum number of attendees to request freebusy for
             "AttendeeRefreshBatch"                : 5, # Number of attendees to do batched refreshes: 0 - no batching
             "AttendeeRefreshBatchDelaySeconds"    : 5, # Time after an iTIP REPLY for first batched attendee refresh
@@ -1035,6 +1039,7 @@
         return configDict
 
 
+
 def _expandPath(path):
     if '$' in path:
         return path.replace('$', getfqdn())

Modified: CalendarServer/trunk/twistedcaldav/test/test_validation.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_validation.py	2013-04-02 18:56:24 UTC (rev 10972)
+++ CalendarServer/trunk/twistedcaldav/test/test_validation.py	2013-04-02 18:59:24 UTC (rev 10973)
@@ -23,6 +23,7 @@
 # to address this to use system twisted.
 from twext.web2.test.test_server import SimpleRequest
 from twext.web2.http import HTTPError
+from twext.web2.resource import Resource
 
 from twistedcaldav.config import config
 from twistedcaldav.ical import Component, Property
@@ -31,16 +32,20 @@
 from twistedcaldav.resource import CalDAVResource
 
 class InMemoryCalendarObjectResource(CalDAVResource):
-    
+
     def exists(self):
         return hasattr(self, "_data") and self._data is not None
 
+
     def iCalendarForUser(self, user):
         return self._data
-    
+
+
     def setData(self, data):
         self._data = data
 
+
+
 class TestCopyMoveValidation(TestCase):
     """
     Tests for the validation code in L{twistedcaldav.method.put_common}.
@@ -52,11 +57,12 @@
         """
 
         self.destination = InMemoryCalendarObjectResource()
-        self.destination.name = lambda : '1'
+        self.destination.name = lambda : 'bar'
         self.destinationParent = CalDAVResource()
-        self.destinationParent.name = lambda : '2'
+        self.destinationParent.name = lambda : 'foo'
         self.destinationParent.isSupportedComponent = lambda x: True
 
+
     def _getSampleCalendar(self):
         return Component.fromString("""BEGIN:VCALENDAR
 VERSION:2.0
@@ -72,9 +78,13 @@
 END:VCALENDAR
 """)
 
+
     def _getStorer(self, calendar):
         self.sampleCalendar = calendar
         req = SimpleRequest(None, "COPY", "http://example.com/foo/bar")
+        req._rememberResource(self.destination, "/foo/bar")
+        req._rememberResource(self.destinationParent, "/foo/")
+        req._rememberResource(Resource(), "/")
         self.storer = StoreCalendarObjectResource(
             req,
             destination=self.destination,
@@ -83,7 +93,8 @@
             calendar=self.sampleCalendar
         )
         return self.storer
-                
+
+
     @inlineCallbacks
     def test_simpleValidRequest(self):
         """
@@ -110,7 +121,7 @@
         eventComponent = list(self.sampleCalendar.subcomponents())[0]
         for x in xrange(config.MaxAttendeesPerInstance):
             eventComponent.addProperty(
-                Property("ATTENDEE", "mailto:user%d at example.com" % (x+3,)))
+                Property("ATTENDEE", "mailto:user%d at example.com" % (x + 3,)))
 
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
@@ -143,7 +154,7 @@
         eventComponent = list(self.sampleCalendar.subcomponents())[0]
         for x in xrange(config.MaxAttendeesPerInstance - 5):
             eventComponent.addProperty(
-                Property("ATTENDEE", "mailto:user%d at example.com" % (x+3,)))
+                Property("ATTENDEE", "mailto:user%d at example.com" % (x + 3,)))
 
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
@@ -155,7 +166,7 @@
         config.MaxAttendeesPerInstance -= 10
         eventComponent.addProperty(
             Property("ATTENDEE", "mailto:user-extra at example.com"))
-        
+
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
         except HTTPError, err:
@@ -187,7 +198,7 @@
         eventComponent = list(self.sampleCalendar.subcomponents())[0]
         for x in xrange(config.MaxAttendeesPerInstance - 5):
             eventComponent.addProperty(
-                Property("ATTENDEE", "mailto:user%d at example.com" % (x+3,)))
+                Property("ATTENDEE", "mailto:user%d at example.com" % (x + 3,)))
 
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
@@ -197,7 +208,7 @@
 
         # Now reduce the limit and try to store without any additional attendees.
         config.MaxAttendeesPerInstance -= 10
-        
+
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
         except HTTPError:
@@ -209,8 +220,8 @@
         eventComponent = list(self.sampleCalendar.subcomponents())[0]
         for x in xrange(config.MaxAttendeesPerInstance + 2):
             eventComponent.addProperty(
-                Property("ATTENDEE", "mailto:user%d at example.com" % (x+3,)))
-        
+                Property("ATTENDEE", "mailto:user%d at example.com" % (x + 3,)))
+
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
         except HTTPError:
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130402/d670be5d/attachment-0001.html>


More information about the calendarserver-changes mailing list