[CalendarServer-changes] [4843] CalendarServer/branches/users/cdaboo/deployment-partition-4722/ twistedcaldav
source_changes at macosforge.org
source_changes at macosforge.org
Tue Dec 8 07:48:08 PST 2009
Revision: 4843
http://trac.macosforge.org/projects/calendarserver/changeset/4843
Author: cdaboo at apple.com
Date: 2009-12-08 07:48:07 -0800 (Tue, 08 Dec 2009)
Log Message:
-----------
Fix auto-accept for deployment partitioning.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/schedule.py
CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/itip.py
CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/scheduler.py
Removed Paths:
-------------
CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/itip.py
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/itip.py 2009-12-08 15:46:55 UTC (rev 4842)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/itip.py 2009-12-08 15:48:07 UTC (rev 4843)
@@ -1,976 +0,0 @@
-##
-# Copyright (c) 2006-2007 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.
-##
-
-"""
-iTIP (RFC2446) processing.
-"""
-
-#
-# This is currently used for handling auto-replies to schedule requests arriving
-# in an inbox. It is called in a delayed fashion via reactor.callLater.
-#
-# We assume that all the components/calendars we deal with have been determined
-# as being 'valid for CalDAV/iTIP', i.e. they contain UIDs, single component
-# types, etc.
-#
-# The logic for component matching needs a lot more work as it currently does not
-# know how to deal with overridden instances.
-#
-
-import datetime
-import md5
-import time
-
-from twisted.python.failure import Failure
-from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
-from twisted.web2.dav import davxml
-from twisted.web2.dav.method.report import NumberOfMatchesWithinLimits
-from twisted.web2.dav.util import joinURL
-from twisted.web2.dav.fileop import delete
-from twisted.web2.dav.resource import AccessDeniedError
-
-from twistedcaldav import caldavxml
-from twistedcaldav.accounting import accountingEnabled, emitAccounting
-from twistedcaldav.log import Logger
-from twistedcaldav.ical import Property, iCalendarProductID
-from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import storeCalendarObjectResource
-from twistedcaldav.resource import isCalendarCollectionResource
-
-log = Logger()
-
-__version__ = "0.0"
-
-__all__ = [
- "handleRequest",
- "canAutoRespond",
-]
-
-class iTipException(Exception):
- pass
-
-def handleRequest(request, principal, inbox, calendar, child):
- """
- Handle an iTIP response automatically.
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- @return: L{Deferred} that is a L{deferredGenerator}
- """
-
- method = calendar.propertyValue("METHOD")
- if method == "REQUEST":
- f = processRequest
- elif method == "ADD":
- f = processAdd
- elif method == "CANCEL":
- f = processCancel
-
- return f(request, principal, inbox, calendar, child)
-
- at inlineCallbacks
-def processRequest(request, principal, inbox, calendar, child):
- """
- Process a METHOD=REQUEST.
-
- Steps:
-
- 1. See if this updates existing ones in Inbox.
- 1. If so,
- 1. Remove existing ones in Inbox.
- 2. See if this updates existing ones in free-busy-set calendars.
- 3. Remove existing ones in those calendars.
- 4. See if this fits into a free slot:
- 1. If not, send REPLY with failure status
- 2. If so
- 1. send REPLY with success
- 2. add to f-b-s calendar
- 2. If not,
- 1. remove the one we got - its 'stale'
- 3. Delete the request from the Inbox.
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- """
-
- log.info("Auto-processing iTIP REQUEST for: %s" % (str(principal),))
- processed = "ignored"
-
- # First determine whether this is a full or partial update. A full update is one containing the master
- # component in a recurrence set (or non-recurring event). Partial is one where overridden instances only are
- # being changed.
-
- new_master = calendar.masterComponent()
-
- # Next we want to try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
- calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-
- if new_master:
- # So we have a full update. That means we need to delete any existing events completely and
- # replace with the ones provided so long as the new one is newer.
-
- # If we have a match then we need to check whether we are updating etc
- check_reply = False
- if calmatch:
- # See whether the new component is older than any existing ones and throw it away if so
- newinfo = (None,) + getComponentSyncInfo(new_master)
- cal = updatecal.iCalendar(calmatch)
- old_master = cal.masterComponent()
- if old_master:
- info = getSyncInfo(calmatch, cal)
- else:
- info = None
- if info is None or compareSyncInfo(info, newinfo) < 0:
- # Existing resource is older and will be replaced
- check_reply = True
- else:
- processed = "older"
- else:
- # We have a new request which we can reply to
- check_reply = True
-
- if check_reply:
- # Process the reply by determining PARTSTAT and sending the reply and booking the event.
- doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
-
- try:
- if accepted:
- if calmatch:
- newchild = yield writeResource(request, calURL, updatecal, calmatch, calendar)
- log.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL))
- else:
- newchild = yield writeResource(request, calURL, updatecal, None, calendar)
- log.info("Added new calendar component in %s." % (calURL,))
- else:
- if calmatch:
- yield deleteResource(updatecal, calmatch)
- log.info("Deleted calendar component %s in %s as update was not accepted." % (calmatch, calURL))
-
- # Send a reply if needed.
- if doreply:
- log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
- newchild = yield writeReply(request, principal, replycal, inbox)
- newInboxResource(child, newchild)
- processed = "processed"
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
- else:
- # So we have a partial update. That means we have to do partial updates to instances in
- # the existing calendar component.
-
- # If we have a match then we need to check whether we are updating etc
- check_reply = False
- if calmatch:
- # Check each component to see whether its new
- cal = updatecal.iCalendar(calmatch)
- old_master = cal.masterComponent()
- processed = "older"
- new_components = [component for component in calendar.subcomponents()]
- for component in new_components:
- if component.name() == "VTIMEZONE":
- continue
-
- newinfo = (None,) + getComponentSyncInfo(component)
- old_component = findMatchingComponent(component, cal)
- if old_component:
- info = (None,) + getComponentSyncInfo(old_component)
- elif old_master:
- info = (None,) + getComponentSyncInfo(old_master)
- else:
- info = None
-
- if info is None or compareSyncInfo(info, newinfo) < 0:
- # Existing resource is older and will be replaced
- check_reply = True
- processed = "processed"
- else:
- calendar.removeComponent(component)
- else:
- # We have a new request which we can reply to
- check_reply = True
-
- if check_reply:
- # Process the reply by determining PARTSTAT and sending the reply and booking the event.
- doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
-
- try:
- if calmatch:
- # Merge the new instances with the old ones
- mergeComponents(calendar, cal)
- newchild = yield writeResource(request, calURL, updatecal, calmatch, cal)
- log.info("Merged calendar component %s with new iTIP message in %s." % (calmatch, calURL))
- else:
- if accepted:
- newchild = yield writeResource(request, calURL, updatecal, None, calendar)
- log.info("Added new calendar component in %s." % (calURL,))
-
- # Do reply if needed.
- if doreply:
- log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
- newchild = yield writeReply(request, principal, replycal, inbox)
- newInboxResource(child, newchild)
-
- processed = "processed"
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
- # Remove the now processed incoming request.
- try:
- yield deleteResource(inbox, child.fp.basename())
- log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
- child.fp.basename(),
- {
- "processed": "processed",
- "older" : "ignored: older",
- "ignored" : "ignored: no match"
- }[processed]
- ))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
-def processAdd(request, principal, inbox, calendar, child):
- """
- Process a METHOD=ADD.
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- """
- log.info("Auto-processing iTIP ADD for: %s" % (str(principal),))
- raise NotImplementedError()
-
- at inlineCallbacks
-def processCancel(request, principal, inbox, calendar, child):
- """
- Process a METHOD=CANCEL.
-
- Policy find all components that match UID, SEQ and R-ID and remove them.
-
- Steps:
-
- 1. See if this updates existing ones in Inbox.
- 2. Remove existing ones in Inbox.
- 3. See if this updates existing ones in free-busy-set calendars.
- 4. Remove existing ones in those calendars.
- 5. Remove the incoming request.
-
- NB Removal can be complex as we need to take RECURRENCE-ID into account - i.e a single
- instance may be cancelled. What we need to do for this is:
-
- 1. If the R-ID of iTIP component matches the R-ID of one in Inbox then it is an exact match, so
- delete the old one.
- 2. If the R-ID of iTIP does not match an R-ID in Inbox, then we are adding a cancellation as an override, so
- leave the new and existing ones in the Inbox.
- 3. If the R-ID of iTIP component matches the R-ID of an overridden component in an f-b-s calendar, then
- remove the overridden component from the f-b-s resource.
- 4. Add an EXDATE to the f-b-s resource to 'cancel' that instance.
-
- TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- """
-
- log.info("Auto-processing iTIP CANCEL for: %s" % (str(principal),))
- processed = "ignored"
-
- # Get all component info for this iTIP message
- newinfo = getSyncInfo(child.fp.basename(), calendar)
- info = getAllInfo(inbox, calendar, child)
-
- # First see if we have a recurrence id which will force extra work
- has_rid = False
- if newinfo[4] is not None:
- has_rid = True
- else:
- for i in info:
- if i[4] is not None:
- has_rid = True
- break
-
- if not has_rid:
- # Compare the new one with each existing one.
- delete_child = yield processOthersInInbox(info, newinfo, inbox, child)
- if delete_child:
- return
-
- # Next we want to try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
- calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-
- # If we have a match then we need to check whether we are updating etc
- if calmatch:
- # See whether the current component is older than any existing ones and throw it away if so
- cal = updatecal.iCalendar(calmatch)
- info = getSyncInfo(calmatch, cal)
- if compareSyncInfo(info, newinfo) < 0:
- # Delete existing resource which has been cancelled
- try:
- yield deleteResource(updatecal, calmatch)
- log.info("Delete calendar component %s in %s as it was cancelled." % (calmatch, calURL))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
- processed = "processed"
- else:
- processed = "older"
- else:
- # Nothing to do except delete the inbox item as we have nothing to cancel.
- processed = "ignored"
- else:
- # Try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
- calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-
- # If we have a match then we need to check whether we are updating etc
- if calmatch:
- # iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
- # So we need to iterate over each iTIP component.
-
- # Get the existing calendar object
- existing_calendar = updatecal.iCalendar(calmatch)
- existing_master = existing_calendar.masterComponent()
- exdates = []
-
- for component in calendar.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
-
- # Find matching component in existing calendar
- old_component = findMatchingComponent(component, existing_calendar)
-
- if old_component:
- # We are cancelling an overridden component, so we need to check the
- # SEQUENCE/DTSAMP with the master.
- if compareComponents(old_component, component) < 0:
- # Exclude the cancelled instance
- exdates.append(component.getRecurrenceIDUTC())
-
- # Remove the existing component.
- existing_calendar.removeComponent(old_component)
- elif existing_master:
- # We are trying to CANCEL a non-overridden instance, so we need to
- # check SEQUENCE/DTSTAMP with the master.
- if compareComponents(existing_master, component) < 0:
- # Exclude the cancelled instance
- exdates.append(component.getRecurrenceIDUTC())
-
- # If we have any EXDATEs lets add them to the existing calendar object and write
- # it back.
- if exdates:
- if existing_master:
- existing_master.addProperty(Property("EXDATE", exdates))
-
- # See if there are still components in the calendar - we might have deleted the last overridden instance
- # in which case the calendar object is empty (except for VTIMEZONEs).
- if existing_calendar.mainType() is None:
- # Delete the now empty calendar object
- yield deleteResource(updatecal, calmatch)
- log.info("Deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL))
- else:
- # Update the existing calendar object
- newchild = yield writeResource(request, calURL, updatecal, calmatch, existing_calendar)
- log.info("Updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL))
- processed = "processed"
- else:
- processed = "older"
- else:
- # Nothing to do except delete the inbox item as we have nothing to cancel.
- processed = "ignored"
-
- # Remove the now processed incoming request.
- try:
- yield deleteResource(inbox, child.fp.basename())
- log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
- child.fp.basename(),
- {
- "processed": "processed",
- "older" : "ignored: older",
- "ignored" : "ignored: no match"
- }[processed]
- ))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
- at inlineCallbacks
-def checkForReply(request, principal, calendar):
- """
- Check whether a reply to the given iTIP message is needed. We will not process a reply
- if RSVP=FALSE. A reply will either be positive (accepted
- invitation) or negative (denied invitation). In addition we will modify calendar to reflect
- any new state (e.g. remove RSVP, set PARTSTAT to ACCEPTED or DECLINED).
-
- BTW The incoming iTIP message may contain multiple components so we need to iterate over all those.
- At the moment we will treat a failure on one instances as a DECLINE of the entire set.
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @return: C{True} if a reply is needed, C{False} otherwise.
- """
-
- # We need to figure out whether the specified component will clash with any others in the f-b-set calendars
- accepted = True
-
- # First expand current one to get instances (only go 1 year into the future)
- default_future_expansion_duration = datetime.timedelta(days=356*1)
- expand_max = datetime.date.today() + default_future_expansion_duration
- instances = calendar.expandTimeRanges(expand_max)
-
- # Extract UID from primary component as we want to ignore this one if we match it
- # in any calendars.
- comp = calendar.mainComponent(allow_multiple=True)
- uid = comp.propertyValue("UID")
-
- # Now compare each instance time-range with the index and see if there is an overlap
- fbset = yield principal.calendarFreeBusyURIs(request)
-
- for calURL in fbset:
- testcal = yield request.locateResource(calURL)
-
- # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
- fbinfo = ([], [], [])
-
- # Now do search for overlapping time-range
- for instance in instances.instances.itervalues():
- try:
- tr = caldavxml.TimeRange(start="20000101", end="20000101")
- tr.start = instance.start
- tr.end = instance.end
- yield report_common.generateFreeBusyInfo(request, testcal, fbinfo, tr, 0, uid)
-
- # If any fbinfo entries exist we have an overlap
- if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
- accepted = False
- break
- except NumberOfMatchesWithinLimits:
- accepted = False
- log.info("Exceeded number of matches whilst trying to find free-time.")
- break
-
- if not accepted:
- break
-
- # Extract the ATTENDEE property matching current recipient from the calendar data
- cuas = principal.calendarUserAddresses()
- attendeeProps = calendar.getAttendeeProperties(cuas)
- if not attendeeProps:
- returnValue((False, None, accepted))
-
- # Look for specific parameters
- rsvp = True
- for attendeeProp in attendeeProps:
- if "RSVP" in attendeeProp.params():
- if attendeeProp.params()["RSVP"][0] == "FALSE":
- rsvp = False
-
- # Now modify the original component
- del attendeeProp.params()["RSVP"]
-
- if accepted:
- partstat = "ACCEPTED"
- else:
- partstat = "DECLINED"
- for attendeeProp in attendeeProps:
- if "PARTSTAT" in attendeeProp.params():
- attendeeProp.params()["PARTSTAT"][0] = partstat
- else:
- attendeeProp.params()["PARTSTAT"] = [partstat]
-
- # Now create a new calendar object for the reply
-
- # First get useful props from the original
- replycal = calendar.duplicate()
-
- # Change METHOD
- replycal.getProperty("METHOD").setValue("REPLY")
-
- # Change PRODID to this server
- replycal.getProperty("PRODID").setValue(iCalendarProductID)
-
- # Add REQUEST-STATUS
- for component in replycal.subcomponents():
- if accepted:
- component.addProperty(Property(name="REQUEST-STATUS", value="2.0; Success."))
- else:
- component.addProperty(Property(name="REQUEST-STATUS", value="4.0; Event conflict. Date/time is busy."))
-
- # Remove all attendees other than ourselves
- for component in replycal.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
- attendeeProp = component.getAttendeeProperty(cuas)
- attendees = tuple(component.properties("ATTENDEE"))
- for attendee in attendees:
- if attendeeProp is None or (attendee.value() != attendeeProp.value()):
- component.removeProperty(attendee)
-
- returnValue((rsvp, replycal, accepted))
-
- at inlineCallbacks
-def writeReply(request, principal, replycal, ainbox):
- """
- Write an iTIP message reply into the specified Inbox.
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param replycal: the L{Component} for the iTIP message reply.
- @param ainbox: the L{ScheduleInboxFile} for the principal's Inbox.
- """
-
- # Get the Inbox of the ORGANIZER
- organizer = replycal.getOrganizer()
- assert organizer is not None
- organizerPrincipal = ainbox.principalForCalendarUserAddress(organizer)
- assert organizerPrincipal is not None
- inboxURL = organizerPrincipal.scheduleInboxURL()
- assert inboxURL
-
- # Determine whether current principal has CALDAV:schedule right on that Inbox
- inbox = yield request.locateResource(inboxURL)
-
- try:
- yield inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef.fromString(principal.principalURL())))
- except AccessDeniedError:
- log.info("Could not send reply as %s does not have CALDAV:schedule permission on %s Inbox." % (principal.principalURL(), organizer))
- returnValue(None)
-
- # Now deposit the new calendar into the inbox
- result = yield writeResource(request, inboxURL, inbox, None, replycal)
-
- if accountingEnabled("iTIP", organizerPrincipal):
- emitAccounting(
- "iTIP", organizerPrincipal,
- "Originator: %s\nRecipients: %s\n\n%s"
- % (principal.principalURL(), str(organizer), str(replycal))
- )
-
- returnValue(result)
-
- at inlineCallbacks
-def writeResource(request, collURL, collection, name, calendar):
- """
- Write out the calendar resource (iTIP) message to the specified calendar, either over-writing the named
- resource or by creating a new one.
-
- @param request: the L{IRequest} for the current request.
- @param collURL: the C{str} containing the URL of the calendar collection.
- @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
- @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
- @param calendar: the L{Component} calendar to write.
- @return: C{tuple} of L{Deferred}, L{CalDAVFile}
- """
-
- # Create a new name if one was not provided
- if name is None:
- name = md5.new(str(calendar) + str(time.time()) + collection.fp.path).hexdigest() + ".ics"
-
- # Get a resource for the new item
- newchildURL = joinURL(collURL, name)
- newchild = yield request.locateResource(newchildURL)
-
- # Modify the original calendar data by removing the METHOD property - everything else is left as-is,
- # as any other needed changes (e.g. RSVP/PARTSTAT) will have been updated.
- # NB Only do this when writing to something other than an Inbox or Outbox
- itipper = True
- if collection.isCalendarCollection():
- method = calendar.getProperty("METHOD")
- if method:
- calendar.removeProperty(method)
- itipper = False
-
- # Now write it to the resource
- try:
- yield storeCalendarObjectResource(
- request=request,
- sourcecal = False,
- destination = newchild,
- destination_uri = newchildURL,
- calendardata = str(calendar),
- destinationparent = collection,
- destinationcal = True,
- isiTIP = itipper
- )
- except:
- # FIXME: bare except
- return
-
- returnValue(newchild)
-
-def newInboxResource(child, newchild):
- """
- Copy recipient and organizer properties from one iTIP resource, to another,
- switching them as appropriate for a reply, and also set the state.
-
- @param child: the L{CalDAVFile} for the original iTIP message.
- @param newchild: the L{CalDAVFile} for the iTIP message reply.
- """
- # Make previous Recipient the new Originator
- if child.hasDeadProperty(caldavxml.Recipient):
- recip = child.readDeadProperty(caldavxml.Recipient)
- if recip.children:
- # Store CALDAV:originator property
- newchild.writeDeadProperty(caldavxml.Originator(davxml.HRef.fromString(str(recip.children[0]))))
-
- # Make previous Originator the new Recipient
- if child.hasDeadProperty(caldavxml.Originator):
- orig = child.readDeadProperty(caldavxml.Originator)
- if orig.children:
- # Store CALDAV:originator property
- newchild.writeDeadProperty(caldavxml.Recipient(davxml.HRef.fromString(str(orig.children[0]))))
-
-def deleteResource(collection, name):
- """
- Delete the calendar resource in the specified calendar.
-
- @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
- @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
- @return: L{Deferred}
- """
-
- delchild = collection.getChild(name)
- index = collection.index()
- index.deleteResource(delchild.fp.basename())
-
- def _deletedResourced(result):
- # Change CTag on the parent calendar collection
- return collection.updateCTag().addCallback(lambda _: result)
-
- d = maybeDeferred(delete, "", delchild.fp, "0")
- d.addCallback(_deletedResourced)
- return d
-
-def canAutoRespond(calendar):
- """
- Check whether the METHOD of this iTIP calendar object is one we can process. Also,
- we will only handle VEVENTs right now.
-
- @param calendar: L{Component} for calendar to examine.
- @return: C{True} if we can auto-respond, C{False} if not.
- """
-
- try:
- method = calendar.propertyValue("METHOD")
- if method not in ("REQUEST", "ADD", "CANCEL"):
- return False
- if calendar.mainType() not in ("VEVENT"):
- return False
- except ValueError:
- return False
-
- return True
-
- at inlineCallbacks
-def processOthersInInbox(info, newinfo, inbox, child):
- # Compare the new one with each existing one.
- delete_child = False
- for i in info:
- # For any that are older, delete them.
- if compareSyncInfo(i, newinfo) < 0:
- try:
- yield deleteResource(inbox, i[0])
- log.info("Deleted iTIP message %s in Inbox that was older than the new one." % (i[0],))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
- else:
- # For any that are newer or the same, mark the new one to be deleted.
- delete_child = True
-
- # Delete the new one if so marked.
- if delete_child:
- try:
- yield deleteResource(inbox, child.fp.basename())
- log.info("Deleted new iTIP message %s in Inbox because it was older than existing ones." % (child.fp.basename(),))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
- returnValue(delete_child)
-
- at inlineCallbacks
-def findCalendarMatch(request, principal, calendar):
- # Try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
-
- # Find the current recipients calendar-free-busy-set
- fbset = yield principal.calendarFreeBusyURIs(request)
-
- # Find the first calendar in the list with a component matching the one we are processing
- calmatch = None
- updatecal = None
- calURL = None
- for calURL in fbset:
- updatecal = yield request.locateResource(calURL)
- if updatecal is None or not updatecal.exists() or not isCalendarCollectionResource(updatecal):
- # We will ignore missing calendars. If the recipient has failed to
- # properly manage the free busy set that should not prevent us from working.
- continue
- calmatch = matchComponentInCalendar(updatecal, calendar)
- if calmatch:
- log.info("Found calendar component %s matching new iTIP message in %s." % (calmatch, calURL))
- break
-
- if calmatch is None and len(fbset):
- calURL = fbset[0]
- updatecal = yield request.locateResource(calURL)
-
- returnValue((calmatch, updatecal, calURL))
-
-def matchComponentInCalendar(collection, calendar):
- """
- See if the component in the provided iTIP calendar object matches any in the specified calendar
- collection.
-
- @param collection: L{CalDAVFile} for the calendar collection to examine.
- @param calendar: L{Component} for calendar to examine.
- @return: C{list} of resource names found.
- """
-
- try:
- # Extract UID from primary component (note we allow multiple components to be present
- # because CANCEL requests can have multiple components).
- comp = calendar.mainComponent(allow_multiple=True)
- uid = comp.propertyValue("UID")
-
- # Now use calendar collection index to find all other resources with the same UID
- index = collection.index()
- result = index.resourceNamesForUID(uid)
-
- # There can be only one
- if len(result) > 0:
- return result[0]
- else:
- return None
- except ValueError:
- return None
-
-def findMatchingComponent(component, calendar):
- """
- See if any overridden component in the provided iTIP calendar object matches the specified component.
-
- @param component: the component to try and match.
- @type component: L{Component}
- @param calendar: the calendar to find a match in.
- @type calendar: L{Component}
- @return: L{Component} for matching component,
- or C{None} if not found.
- """
-
- # Extract RECURRENCE-ID value from component
- rid = component.getRecurrenceIDUTC()
-
- # Return the one that matches in the calendar
- return calendar.overriddenComponent(rid)
-
-def mergeComponents(newcal, oldcal):
- """
- Merge the overridden instance components in newcal into oldcal replacing any
- matching components there.
-
- @param newcal: the new overridden instances to use.
- @type newcal: L{Component}
- @param oldcal: the component to merge into.
- @type oldcal: L{Component}
- """
-
- # FIXME: going to ignore VTIMEZONE - i.e. will assume that the component being added
- # use a TZID that is already specified in the old component set.
-
- # We will update the SEQUENCE on the master to the highest value of the current one on the master
- # or the ones in the components we are changing.
-
- for component in newcal.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
-
- rid = component.getRecurrenceIDUTC()
- old_component = oldcal.overriddenComponent(rid)
- if old_component:
- oldcal.removeComponent(old_component)
- oldcal.addComponent(component)
-
-def getAllInfo(collection, calendar, ignore):
- """
- Find each component in the calendar collection that has a matching UID with
- the supplied component, and get useful synchronization details from it, ignoring
- the one with the supplied resource name.
-
- @param collection: the L{CalDAVFile} for the calendar collection.
- @param calendar: the L{Component} for the component being compared with.
- @param ignore: the C{str} containing the name of a resource to ignore,
- or C{None} if none to ignore.
- @return: C{list} of synchronization information for each resource found.
- """
- names = []
- try:
- # Extract UID from primary component (note we allow multiple components to be present
- # because CANCEL requests can have multiple components).
- comp = calendar.mainComponent(allow_multiple=True)
- uid = comp.propertyValue("UID")
-
- # Now use calendar collection index to find all other resources with the same UID
- index = collection.index()
- names = index.resourceNamesForUID(uid)
-
- # Remove the one we want to ignore
- if ignore is not None:
- names = [name for name in names if name != ignore.fp.basename()]
- except ValueError:
- return []
-
- # Now get info for each name
- result = []
- for name in names:
- cal = collection.iCalendar(name)
- result.append(getSyncInfo(name, cal))
-
- return result
-
-def getSyncInfo(name, calendar):
- """
- Get property value details needed to synchronize iTIP components.
-
- @param calendar: L{Component} for calendar to check.
- @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
- """
- try:
- # Extract components from primary component (note we allow multiple components to be present
- # because CANCEL requests can have multiple components).
- comp = calendar.mainComponent(allow_multiple=True)
- uid, seq, dtstamp, rid = getComponentSyncInfo(comp)
-
- except ValueError:
- return (name, None, None, None, None)
-
- return (name, uid, seq, dtstamp, rid)
-
-def getComponentSyncInfo(component):
- """
- Get property value details needed to synchronize iTIP components.
-
- @param component: L{Component} to check.
- @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
- """
- try:
- # Extract items from component
- uid = component.propertyValue("UID")
- seq = component.propertyValue("SEQUENCE")
- if seq:
- seq = int(seq)
- dtstamp = component.propertyValue("DTSTAMP")
- rid = component.propertyValue("RECURRENCE-ID")
-
- except ValueError:
- return (None, None, None, None)
-
- return (uid, seq, dtstamp, rid)
-
-def compareComponents(component1, component2):
- """
- Compare synchronization information for two components to see if they match according to iTIP.
-
- @param component1: first component to check.
- @type component1: L{Component}
- @param component2: second component to check.
- @type component2: L{Component}
-
- @return: 0, 1, -1 as per compareSyncInfo.
- """
- info1 = (None,) + getComponentSyncInfo(component1)
- info2 = (None,) + getComponentSyncInfo(component2)
- return compareSyncInfo(info1, info2)
-
-def compareSyncInfo(info1, info2):
- """
- Compare two synchronization information records.
-
- @param info1: a C{tuple} as returned by L{getSyncInfo}.
- @param info2: a C{tuple} as returned by L{getSyncInfo}.
- @return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
- """
- # UIDs MUST match
- assert info1[1] == info2[1]
-
- # Look for sequence
- if (info1[2] is not None) and (info2[2] is not None):
- if info1[2] > info2[2]:
- return 1
- if info1[2] < info2[2]:
- return -1
- elif (info1[2] is not None) and (info2[2] is None):
- return 1
- elif (info1[2] is None) and (info2[2] is not None):
- return -1
-
- # Look for DTSTAMP
- if (info1[3] is not None) and (info2[3] is not None):
- if info1[3] > info2[3]:
- return 1
- if info1[3] < info2[3]:
- return -1
- elif (info1[3] is not None) and (info2[3] is None):
- return 1
- elif (info1[3] is None) and (info2[3] is not None):
- return -1
-
- return 0
-
-class iTIPRequestStatus(object):
- """
- String constants for various iTIP status codes we use.
- """
-
- MESSAGE_PENDING_CODE = "1.0"
- MESSAGE_SENT_CODE = "1.1"
- MESSAGE_DELIVERED_CODE = "1.2"
-
- MESSAGE_PENDING = MESSAGE_PENDING_CODE + ";Scheduling message send is pending"
- MESSAGE_SENT = MESSAGE_SENT_CODE + ";Scheduling message has been sent"
- MESSAGE_DELIVERED = MESSAGE_DELIVERED_CODE + ";Scheduling message has been delivered"
-
- SUCCESS = "2.0;Success"
-
- INVALID_CALENDAR_USER = "3.7;Invalid Calendar User"
- NO_AUTHORITY = "3.8;No authority"
-
- BAD_REQUEST = "5.0;Service cannot handle request"
- SERVICE_UNAVAILABLE = "5.1;Service unavailable"
- INVALID_SERVICE = "5.2;Invalid calendar service"
- NO_USER_SUPPORT = "5.3;No scheduling support for user"
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/schedule.py 2009-12-08 15:46:55 UTC (rev 4842)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/schedule.py 2009-12-08 15:48:07 UTC (rev 4843)
@@ -43,6 +43,7 @@
from twistedcaldav.log import LoggingMixIn
from twistedcaldav.resource import CalDAVResource
from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.scheduling.itip import handleRequest
from twistedcaldav.scheduling.scheduler import CalDAVScheduler, IScheduleScheduler
@@ -199,7 +200,7 @@
# Now process each one in order
for child, calendar in children:
- reactor.callLater(0.0, itip.handleRequest, *(request, principal, self, calendar, child)) #@UndefinedVariable
+ reactor.callLater(0.0, handleRequest, *(request, principal, self, calendar, child)) #@UndefinedVariable
if (len(children)):
response_text = "Started auto-processing of %d iTIP messages." % (len(children,))
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/itip.py 2009-12-08 15:46:55 UTC (rev 4842)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/itip.py 2009-12-08 15:48:07 UTC (rev 4843)
@@ -169,7 +169,8 @@
if doreply:
log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
newchild = yield writeReply(request, principal, replycal, inbox)
- newInboxResource(child, newchild)
+ if newchild:
+ newInboxResource(child, newchild)
processed = "processed"
except:
# FIXME: bare except
@@ -230,7 +231,8 @@
if doreply:
log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
newchild = yield writeReply(request, principal, replycal, inbox)
- newInboxResource(child, newchild)
+ if newchild:
+ newInboxResource(child, newchild)
processed = "processed"
except:
@@ -560,18 +562,30 @@
inboxURL = organizerPrincipal.scheduleInboxURL()
assert inboxURL
- # Determine whether current principal has CALDAV:schedule right on that Inbox
- inbox = yield request.locateResource(inboxURL)
-
- try:
- yield inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef.fromString(principal.principalURL())))
- except AccessDeniedError:
- log.info("Could not send reply as %s does not have CALDAV:schedule permission on %s Inbox." % (principal.principalURL(), organizer))
- returnValue(None)
+ # Check for local or partitioned organizer
+ if organizerPrincipal.locallyHosted():
+ # Determine whether current principal has CALDAV:schedule right on that Inbox
+ inbox = yield request.locateResource(inboxURL)
- # Now deposit the new calendar into the inbox
- result = yield writeResource(request, inboxURL, inbox, None, replycal)
+ try:
+ yield inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef.fromString(principal.principalURL())))
+ except AccessDeniedError:
+ log.info("Could not send reply as %s does not have CALDAV:schedule permission on %s Inbox." % (principal.principalURL(), organizer))
+ returnValue(None)
+
+ # Now deposit the new calendar into the inbox
+ result = yield writeResource(request, inboxURL, inbox, None, replycal)
+ else:
+ # Send reply to Organizer
+ # This is a local CALDAV scheduling operation.
+ from twistedcaldav.scheduling.scheduler import CalDAVScheduler
+ scheduler = CalDAVScheduler(request, ainbox)
+
+ # Do the POST processing treating
+ yield scheduler.doSchedulingViaPUT(tuple(principal.calendarUserAddresses())[0], (organizer,), replycal, True)
+ result = None
+
if accountingEnabled("iTIP", organizerPrincipal):
emitAccounting(
"iTIP", organizerPrincipal,
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/scheduler.py 2009-12-08 15:46:55 UTC (rev 4842)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/scheduler.py 2009-12-08 15:48:07 UTC (rev 4843)
@@ -105,6 +105,24 @@
result = (yield self.doScheduling())
returnValue(result)
+ def doSchedulingViaPUT(self, originator, recipients, calendar, internal_request=False):
+ """
+ The implicit scheduling PUT operation.
+ """
+
+ self.method = "PUT"
+
+ # Load various useful bits doing some basic checks on those
+ self.originator = originator
+ self.recipients = recipients
+ self.calendar = calendar
+ self.internal_request = internal_request
+
+ # Do some extra authorization checks
+ self.checkAuthorization()
+
+ return self.doScheduling()
+
@inlineCallbacks
def doScheduling(self):
# Check validity of Originator header.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20091208/cfcd4df1/attachment-0001.html>
More information about the calendarserver-changes
mailing list