Logo Search packages:      
Sourcecode: calendarserver version File versions  Download package

def twistedcaldav::schedule::ScheduleOutboxResource::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.

Definition at line 190 of file schedule.py.

00190                                 :
        """
        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
        x = waitForDeferred(self.authorize(request, (caldavxml.Schedule(),)))
        yield x
        x.getResult()

        # Must be content-type text/calendar
        contentType = request.headers.getHeader("content-type")
        if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
            log.err("MIME type %s not allowed in calendar collection" % (contentType,))
            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)
        originatorPrincipal = self.principalForCalendarUserAddress(originator)
        if originatorPrincipal is None:
            log.err("Could not find principal for originator: %s" % (originator,))
            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))

        inboxURL = originatorPrincipal.scheduleInboxURL()
        if inboxURL is None:
            log.err("Could not find inbox for originator: %s" % (originator,))
            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
    
        # Verify that Originator matches the authenticated user
        if davxml.Principal(davxml.HRef(originatorPrincipal.principalURL())) != self.currentPrincipal(request):
            log.err("Originator: %s does not match authorized user: %s" % (originator, self.currentPrincipal(request).children[0],))
            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")
        recipientsState = {"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")))

        # X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs
        if calendar.hasProperty(Component.ACCESS_PROPERTY):
            log.err("X-CALENDARSERVER-ACCESS not allowed in a calendar component POST request: %s" % (calendar,), system="CalDAV Outbox POST")
            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
    
        # Verify that the ORGANIZER's cu address maps to the request.uri
        organizer = calendar.getOrganizer()
        if organizer is None:
            organizerPrincipal = None
        else:
            organizerPrincipal = self.principalForCalendarUserAddress(organizer)

        if organizerPrincipal 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
            organizerPrincipal.record != self.parent.record
        ):
            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
            attendeePrincipal = self.principalForCalendarUserAddress(attendees[0])
            if attendeePrincipal is None or attendeePrincipal.record != self.parent.record:
                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

            # Look for maksed UID
            excludeUID = calendar.getMaskUID()

            # Do free busy operation
            freebusy = True
        else:
            # Do regular invite (fan-out)
            freebusy = False

        # Prepare for multiple responses
        responses = ScheduleResponseQueue("POST", responsecode.OK)
    
        # 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 None:
                log.err("No schedulable principal for calendar user address: %s" % (recipient,))
            else:
                inboxURL = principal.scheduleInboxURL()
                if inboxURL:
                    inbox = waitForDeferred(request.locateResource(inboxURL))
                    yield inbox
                    inbox = inbox.getResult()
                else:
                    log.err("No schedule inbox for principal: %s" % (principal,))

            if inbox is None:
                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
                responses.add(recipient, Failure(exc_value=err), reqstatus="3.7;Invalid Calendar User")
                recipientsState["BAD"] += 1
            
                # Process next recipient
                continue
            else:
                #
                # Check access controls
                #
                try:
                    d = waitForDeferred(inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef(organizerPrincipal.principalURL()))))
                    yield d
                    d.getResult()
                except AccessDeniedError:
                    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")
                    recipientsState["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:
                        # Process the availability property from the Inbox.
                        has_prop = waitForDeferred(inbox.hasProperty((calendarserver_namespace, "calendar-availability"), request))
                        yield has_prop
                        has_prop = has_prop.getResult()
                        if has_prop:
                            availability = waitForDeferred(inbox.readProperty((calendarserver_namespace, "calendar-availability"), request))
                            yield availability
                            availability = availability.getResult()
                            availability = availability.calendar()
                            report_common.processAvailabilityFreeBusy(availability, fbinfo, timeRange)

                        # Check to see if the recipient is the same calendar user as the organizer.
                        # Needed for masked UID stuff.
                        same_calendar_user = organizerPrincipal.principalURL() == principal.principalURL()

                        # Now process free-busy set calendars
                        matchtotal = 0
                        for calendarResourceURL in fbset:
                            calendarResource = waitForDeferred(request.locateResource(calendarResourceURL))
                            yield calendarResource
                            calendarResource = calendarResource.getResult()
                            if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
                                # 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,
                                calendarResource,
                                fbinfo,
                                timeRange,
                                matchtotal,
                                excludeuid = excludeUID,
                                organizer = organizer,
                                same_calendar_user = same_calendar_user
                            ))
                            yield matchtotal
                            matchtotal = matchtotal.getResult()
                    
                        # Build VFREEBUSY iTIP reply for this recipient
                        fbresult = report_common.buildFreeBusyResult(
                            fbinfo,
                            timeRange,
                            organizer = calendar.getOrganizerProperty(),
                            attendee = attendeeProp,
                            uid = calendar.resourceUID(),
                            method="REPLY"
                        )

                        responses.add(recipient, responsecode.OK, reqstatus="2.0;Success", calendar=fbresult)
                        recipientsState["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")
                        recipientsState["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")
                        recipientsState["OK"] += 1
        
                        # Store CALDAV:originator property
                        child.writeDeadProperty(caldavxml.Originator(davxml.HRef(originator)))
                    
                        # Store CALDAV:recipient property
                        child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient)))
                    
                        # Look for auto-schedule option
                        if principal.autoSchedule():
                            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")
                        recipientsState["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(0.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):


Generated by  Doxygen 1.6.0   Back to index