[CalendarServer-changes] [707]
CalendarServer/trunk/twistedcaldav/schedule.py
source_changes at macosforge.org
source_changes at macosforge.org
Tue Dec 5 16:41:43 PST 2006
Revision: 707
http://trac.macosforge.org/projects/calendarserver/changeset/707
Author: wsanchez at apple.com
Date: 2006-12-05 16:41:43 -0800 (Tue, 05 Dec 2006)
Log Message:
-----------
Need schedule.py, too, eh?
Added Paths:
-----------
CalendarServer/trunk/twistedcaldav/schedule.py
Added: CalendarServer/trunk/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/schedule.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/schedule.py 2006-12-06 00:41:43 UTC (rev 707)
@@ -0,0 +1,518 @@
+##
+# Copyright (c) 2005-2006 Apple Computer, 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.
+#
+# DRI: Cyrus Daboo, cdaboo at apple.com
+##
+
+"""
+CalDAV scheduling resources.
+"""
+
+__all__ = [
+ "ScheduleInboxResource",
+ "ScheduleOutboxResource",
+]
+
+from twisted.internet import reactor
+from twisted.internet.defer import deferredGenerator, maybeDeferred, waitForDeferred
+from twisted.python import log
+from twisted.python.failure import Failure
+from twisted.web2 import responsecode
+from twisted.web2.http import HTTPError, Response
+from twisted.web2.http_headers import MimeType
+from twisted.web2.dav import davxml
+from twisted.web2.dav.http import ErrorResponse, errorForFailure, messageForFailure, statusForFailure
+from twisted.web2.dav.util import joinURL, parentForURL
+
+from twistedcaldav import caldavxml
+from twistedcaldav import customxml
+from twistedcaldav import itip
+from twistedcaldav.resource import CalDAVResource
+from twistedcaldav.caldavxml import caldav_namespace, TimeRange
+from twistedcaldav.ical import Component
+from twistedcaldav.method import report_common
+from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.resource import isCalendarCollectionResource
+
+import md5
+import time
+
+class CalendarSchedulingCollectionResource (CalDAVResource):
+ """
+ CalDAV principal resource.
+
+ Extends L{DAVResource} to provide CalDAV scheduling collection
+ functionality.
+ """
+ def isCollection(self):
+ return True
+
+ def isCalendarCollection(self):
+ return False
+
+ def isPseudoCalendarCollection(self):
+ return True
+
+ def supportedReports(self):
+ result = super(CalDAVResource, self).supportedReports()
+ result.append(davxml.Report(caldavxml.CalendarQuery(),))
+ result.append(davxml.Report(caldavxml.CalendarMultiGet(),))
+ # free-busy report not allowed
+ return result
+
+class ScheduleInboxResource (CalendarSchedulingCollectionResource):
+ """
+ CalDAV schedule Inbox resource.
+
+ Extends L{DAVResource} to provide CalDAV functionality.
+ """
+ def resourceType(self):
+ return davxml.ResourceType(davxml.Collection(), caldavxml.ScheduleInbox())
+
+ def defaultAccessControlList(self):
+ return davxml.ACL(
+ # CalDAV:schedule for any authenticated user
+ davxml.ACE(
+ davxml.Principal(davxml.Authenticated()),
+ davxml.Grant(
+ davxml.Privilege(caldavxml.Schedule()),
+ ),
+ ),
+ )
+
+class ScheduleOutboxResource (CalendarSchedulingCollectionResource):
+ """
+ CalDAV schedule Outbox resource.
+
+ Extends L{DAVResource} to provide CalDAV functionality.
+ """
+ def resourceType(self):
+ return davxml.ResourceType(davxml.Collection(), caldavxml.ScheduleOutbox())
+
+ @deferredGenerator
+ def http_POST(self, request):
+ """
+ The CalDAV POST method.
+
+ This uses a generator function yielding either L{waitForDeferred} objects or L{Response} objects.
+ This allows for code that follows a 'linear' execution pattern rather than having to use nested
+ L{Deferred} callbacks. The logic is easier to follow this way plus we don't run into deep nesting
+ issues which the other approach would have with large numbers of recipients.
+ """
+ # Check authentication and access controls
+ parent = waitForDeferred(request.locateResource(parentForURL(request.uri)))
+ yield parent
+ parent = parent.getResult()
+ x = waitForDeferred(parent.authorize(request, (caldavxml.Schedule(),)))
+
+ # Must be content-type text/calendar
+ content_type = request.headers.getHeader("content-type")
+ if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "calendar"):
+ log.err("MIME type %s not allowed in calendar collection" % (content_type,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
+
+ # Must have Originator header
+ originator = request.headers.getRawHeaders("originator")
+ if originator is None or (len(originator) != 1):
+ log.err("POST request must have Originator header")
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
+ else:
+ originator = originator[0]
+
+ # Verify that Originator is a valid calendar user (has an INBOX)
+ inboxURL = None
+ oprincipal = self.principalForCalendarUserAddress(originator)
+ if oprincipal is not None:
+ inboxURL = oprincipal.scheduleInboxURL()
+ if inboxURL is None:
+ log.err("Could not find Inbox for originator: %s" % (originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+ # Get list of Recipient headers
+ rawrecipients = request.headers.getRawHeaders("recipient")
+ if rawrecipients is None or (len(rawrecipients) == 0):
+ log.err("POST request must have at least one Recipient header")
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
+
+ # Recipient header may be comma separated list
+ recipients = []
+ for rawrecipient in rawrecipients:
+ for r in rawrecipient.split(","):
+ r = r.strip()
+ if len(r):
+ recipients.append(r)
+
+ timerange = TimeRange(start="20000101", end="20000102")
+ recipients_state = {"OK":0, "BAD":0}
+
+ # Parse the calendar object from the HTTP request stream
+ try:
+ d = waitForDeferred(Component.fromIStream(request.stream))
+ yield d
+ calendar = d.getResult()
+ except:
+ log.err("Error while handling POST: %s" % (Failure(),))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+ # Must be a valid calendar
+ try:
+ calendar.validCalendarForCalDAV()
+ except ValueError:
+ log.err("POST request calendar component is not valid: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+ # Must have a METHOD
+ if not calendar.isValidMethod():
+ log.err("POST request must have valid METHOD property in calendar component: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+ # Verify iTIP behaviour
+ if not calendar.isValidITIP():
+ log.err("POST request must have a calendar component that satisfies iTIP requirements: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+ # Verify that the ORGANIZER's cu address maps to the request.uri
+ outboxURL = None
+ organizer = calendar.getOrganizer()
+ if organizer is not None:
+ oprincipal = self.principalForCalendarUserAddress(organizer)
+ if oprincipal is not None:
+ outboxURL = oprincipal.scheduleOutboxURL()
+ if outboxURL is None:
+ log.err("ORGANIZER in calendar data is not valid: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+ # Prevent spoofing of ORGANIZER with specific METHODs
+ if (calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER")) and (outboxURL != request.uri):
+ log.err("ORGANIZER in calendar data does not match owner of Outbox: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+ # Prevent spoofing when doing reply-like METHODs
+ if calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
+ # Verify that there is a single ATTENDEE property and that the Originator has permission
+ # to send on behalf of that ATTENDEE
+ attendees = calendar.getAttendees()
+
+ # Must have only one
+ if len(attendees) != 1:
+ log.err("ATTENDEE list in calendar data is wrong: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+
+ # Attendee's Outbox MUST be the request URI
+ aoutboxURL = None
+ aprincipal = self.principalForCalendarUserAddress(attendees[0])
+ if aprincipal is not None:
+ aoutboxURL = aprincipal.scheduleOutboxURL()
+ if aoutboxURL is None or aoutboxURL != request.uri:
+ log.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+
+ # For free-busy do immediate determination of iTIP result rather than fan-out
+ if (calendar.propertyValue("METHOD") == "REQUEST") and (calendar.mainType() == "VFREEBUSY"):
+ # Extract time range from VFREEBUSY object
+ vfreebusies = [v for v in calendar.subcomponents() if v.name() == "VFREEBUSY"]
+ if len(vfreebusies) != 1:
+ log.err("iTIP data is not valid for a VFREEBUSY request: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+ dtstart = vfreebusies[0].getStartDateUTC()
+ dtend = vfreebusies[0].getEndDateUTC()
+ if dtstart is None or dtend is None:
+ log.err("VFREEBUSY start/end not valid: %s" % (calendar,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+ timerange.start = dtstart
+ timerange.end = dtend
+
+ # Do free busy operation
+ freebusy = True
+ else:
+ # Do regular invite (fan-out)
+ freebusy = False
+
+ # Prepare for multiple responses
+ responses = ScheduleResponseQueue("POST", responsecode.OK)
+
+ # Outbox copy is saved when not doing free busy request
+ if not freebusy:
+ # Hash the iCalendar data for use as the last path element of the URI path
+ name = md5.new(str(calendar) + str(time.time()) + self.fp.path).hexdigest() + ".ics"
+
+ # Save a copy of the calendar data into the Outbox
+ childURL = joinURL(request.uri, name)
+ child = waitForDeferred(request.locateResource(childURL))
+ yield child
+ child = child.getResult()
+ responses.setLocation(childURL)
+
+ try:
+ d = waitForDeferred(
+ maybeDeferred(
+ storeCalendarObjectResource,
+ request=request,
+ sourcecal = False,
+ destination = child,
+ destination_uri = childURL,
+ calendardata = str(calendar),
+ destinationparent = self,
+ destinationcal = True,
+ isiTIP = True
+ )
+ )
+ yield d
+ d.getResult()
+ except:
+ log.err("Error while handling POST: %s" % (Failure(),))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "outbox-copy")))
+
+ # Store CALDAV:originator property
+ child.writeDeadProperty(caldavxml.Originator(davxml.HRef(originator)))
+
+ # Store CALDAV:recipient property
+ child.writeDeadProperty(caldavxml.Recipient(*map(davxml.HRef, recipients)))
+
+ # Extract the ORGANIZER property and UID value from the calendar data for use later
+ organizerProp = calendar.getOrganizerProperty()
+ uid = calendar.resourceUID()
+
+ # Loop over each recipient and do appropriate action.
+ autoresponses = []
+ for recipient in recipients:
+ # Get the principal resource for this recipient
+ principal = self.principalForCalendarUserAddress(recipient)
+
+ # Map recipient to their inbox
+ inbox = None
+ if principal is not None:
+ inboxURL = principal.scheduleInboxURL()
+ if inboxURL:
+ inbox = waitForDeferred(request.locateResource(inboxURL))
+ yield inbox
+ inbox = inbox.getResult()
+ if inbox is None:
+ log.err("Could not find Inbox for recipient: %s" % (recipient,))
+ err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
+ responses.add(recipient, Failure(exc_value=err), reqstatus="3.7;Invalid Calendar User")
+ recipients_state["BAD"] += 1
+
+ # Process next recipient
+ continue
+ else:
+
+ #
+ # Check access controls
+ #
+ try:
+ d = waitForDeferred(inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef(oprincipal))))
+ yield d
+ d.getResult()
+ except:
+ log.err("Could not access Inbox for recipient: %s" % (recipient,))
+ err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permisions")))
+ responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
+ recipients_state["BAD"] += 1
+
+ # Process next recipient
+ continue
+
+ # Different behaviour for free-busy vs regular invite
+ if freebusy:
+ # Extract the ATTENDEE property matching current recipient from the calendar data
+ cuas = principal.calendarUserAddresses()
+ attendeeProp = calendar.getAttendeeProperty(cuas)
+
+ # Find the current recipients calendar-free-busy-set
+ fbset = waitForDeferred(principal.calendarFreeBusyURIs(request))
+ yield fbset
+ fbset = fbset.getResult()
+
+ # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+ fbinfo = ([], [], [])
+
+ try:
+ matchtotal = 0
+ for calURL in fbset:
+ cal = waitForDeferred(request.locateResource(calURL))
+ yield cal
+ cal = cal.getResult()
+ if cal is None or not cal.exists() or not isCalendarCollectionResource(cal):
+ # 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
+
+ matchtotal = waitForDeferred(report_common.generateFreeBusyInfo(request, cal, fbinfo, timerange, matchtotal))
+ yield matchtotal
+ matchtotal = matchtotal.getResult()
+
+ # Build VFREEBUSY iTIP reply for this recipient
+ fbresult = report_common.buildFreeBusyResult(fbinfo, timerange, organizer=organizerProp, attendee=attendeeProp, uid=uid)
+
+ responses.add(recipient, responsecode.OK, reqstatus="2.0;Success", calendar=fbresult)
+ recipients_state["OK"] += 1
+
+ except:
+ log.err("Could not determine free busy information: %s" % (recipient,))
+ err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
+ responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
+ recipients_state["BAD"] += 1
+
+ else:
+ # Hash the iCalendar data for use as the last path element of the URI path
+ name = md5.new(str(calendar) + str(time.time()) + inbox.fp.path).hexdigest() + ".ics"
+
+ # Get a resource for the new item
+ childURL = joinURL(inboxURL, name)
+ child = waitForDeferred(request.locateResource(childURL))
+ yield child
+ child = child.getResult()
+
+ # Copy calendar to inbox (doing fan-out)
+ d = waitForDeferred(
+ maybeDeferred(
+ storeCalendarObjectResource,
+ request=request,
+ sourcecal = False,
+ destination = child,
+ destination_uri = childURL,
+ calendardata = str(calendar),
+ destinationparent = inbox,
+ destinationcal = True,
+ isiTIP = True
+ )
+ )
+ yield d
+ try:
+ d.getResult()
+ responses.add(recipient, responsecode.OK, reqstatus="2.0;Success")
+ recipients_state["OK"] += 1
+
+ # Store CALDAV:originator property
+ child.writeDeadProperty(caldavxml.Originator(davxml.HRef(originator)))
+
+ # Store CALDAV:recipient property
+ child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient)))
+
+ # Store CALDAV:schedule-state property
+ child.writeDeadProperty(caldavxml.ScheduleState(caldavxml.NotProcessed()))
+
+ # Look for auto-respond option
+ if inbox.hasDeadProperty(customxml.TwistedScheduleAutoRespond):
+ autoresponses.append((principal, inbox, child))
+ except:
+ log.err("Could not store data in Inbox : %s" % (inbox,))
+ err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
+ responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
+ recipients_state["BAD"] += 1
+
+ # Now we have to do auto-respond
+ if len(autoresponses) != 0:
+ # First check that we have a method that we can auto-respond to
+ if not itip.canAutoRespond(calendar):
+ autoresponses = []
+
+ # Now do the actual auto response
+ for principal, inbox, child in autoresponses:
+ # Add delayed reactor task to handle iTIP responses
+ reactor.callLater(5.0, itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
+ #reactor.callInThread(itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
+
+ # Return with final response if we are done
+ yield responses.response()
+
+class ScheduleResponseResponse (Response):
+ """
+ ScheduleResponse L{Response} object.
+ Renders itself as a CalDAV:schedule-response XML document.
+ """
+ def __init__(self, xml_responses, location=None):
+ """
+ @param xml_responses: an interable of davxml.Response objects.
+ @param location: the value of the location header to return in the response,
+ or None.
+ """
+
+ Response.__init__(self, code=responsecode.OK,
+ stream=caldavxml.ScheduleResponse(*xml_responses).toxml())
+
+ self.headers.setHeader("content-type", MimeType("text", "xml"))
+
+ if location is not None:
+ self.headers.setHeader("location", location)
+
+class ScheduleResponseQueue (object):
+ """
+ Stores a list of (typically error) responses for use in a
+ L{ScheduleResponse}.
+ """
+ def __init__(self, method, success_response):
+ """
+ @param method: the name of the method generating the queue.
+ @param success_response: the response to return in lieu of a
+ L{ScheduleResponse} if no responses are added to this queue.
+ """
+ self.responses = []
+ self.method = method
+ self.success_response = success_response
+ self.location = None
+
+ def setLocation(self, location):
+ """
+ @param location: the value of the location header to return in the response,
+ or None.
+ """
+ self.location = location
+
+ def add(self, recipient, what, reqstatus=None, calendar=None):
+ """
+ Add a response.
+ @param recipient: the recipient for this response.
+ @param what: a status code or a L{Failure} for the given recipient.
+ @param status: the iTIP request-status for the given recipient.
+ @param calendar: the calendar data for the given recipient response.
+ """
+ if type(what) is int:
+ code = what
+ error = None
+ message = responsecode.RESPONSES[code]
+ elif isinstance(what, Failure):
+ code = statusForFailure(what)
+ error = errorForFailure(what)
+ message = messageForFailure(what)
+ else:
+ raise AssertionError("Unknown data type: %r" % (what,))
+
+ if code > 400: # Error codes only
+ log.err("Error during %s for %s: %s" % (self.method, recipient, message))
+
+ children = []
+ children.append(caldavxml.Recipient(davxml.HRef.fromString(recipient)))
+ children.append(caldavxml.RequestStatus(reqstatus))
+ if calendar is not None:
+ children.append(caldavxml.CalendarData.fromCalendar(calendar))
+ if error is not None:
+ children.append(error)
+ if message is not None:
+ children.append(davxml.ResponseDescription(message))
+ self.responses.append(caldavxml.Response(*children))
+
+ def response(self):
+ """
+ Generate a L{ScheduleResponseResponse} with the responses contained in the
+ queue or, if no such responses, return the C{success_response} provided
+ to L{__init__}.
+ @return: the response.
+ """
+ if self.responses:
+ return ScheduleResponseResponse(self.responses, self.location)
+ else:
+ return self.success_response
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20061205/4f3a2ea0/attachment.html
More information about the calendarserver-changes
mailing list