def test_freebusy(self): """ Test that action=component works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) fbstart = "{now:04d}0102T000000Z".format(**self.nowYear) fbend = "{now:04d}0103T000000Z".format(**self.nowYear) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") fbinfo = FreebusyQuery.FBInfo([], [], []) timerange = Period(DateTime.parseText(fbstart), DateTime.parseText(fbend)) organizer = recipient = (yield calendarUserFromCalendarUserAddress("mailto:[email protected]", self.theTransactionUnderTest(1))) freebusy = FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange) matchtotal = (yield freebusy.generateFreeBusyInfo([shared, ], fbinfo)) self.assertEqual(matchtotal, 1) self.assertEqual(fbinfo[0], [Period.parseText("{now:04d}0102T140000Z/PT1H".format(**self.nowYear)), ]) self.assertEqual(len(fbinfo[1]), 0) self.assertEqual(len(fbinfo[2]), 0) yield self.commitTransaction(1)
def report_urn_ietf_params_xml_ns_caldav_free_busy_query(self, request, freebusy): """ Generate a free-busy REPORT. (CalDAV-access-09, section 7.8) """ if not self.isCollection(): log.error("freebusy report is only allowed on collection resources {s!r}", s=self) raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Not a calendar collection")) if freebusy.qname() != (caldavxml.caldav_namespace, "free-busy-query"): raise ValueError("{CalDAV:}free-busy-query expected as root element, not %s." % (freebusy.sname(),)) timerange = freebusy.timerange if not timerange.valid(): raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid time-range specified")) fbset = [] accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes()) if accepted_type is None: raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type")) def getCalendarList(calresource, uri): # @UnusedVariable """ Store the calendars that match the query in L{fbset} which will then be used with the freebusy query. @param calresource: the L{CalDAVResource} for a calendar collection. @param uri: the uri for the calendar collection resource. """ fbset.append(calresource._newStoreObject) return succeed(True) # Run report taking depth into account depth = request.headers.getHeader("depth", "0") yield report_common.applyToCalendarCollections(self, request, request.uri, depth, getCalendarList, (caldavxml.ReadFreeBusy(),)) # Do the actual freebusy query against the set of matched calendars principal = yield self.resourceOwnerPrincipal(request) organizer = recipient = LocalCalendarUser(principal.canonicalCalendarUserAddress(), principal.record) timerange = Period(timerange.start, timerange.end) try: fbresult = yield FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange).generateAttendeeFreeBusyResponse(fbset=fbset, method=None) except NumberOfMatchesWithinLimits: log.error("Too many matching components in free-busy report") raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits(), "Too many components" )) except TimeRangeLowerLimit, e: raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, caldavxml.MinDateTime(), "Time-range value too far in the past. Must be on or after %s." % (str(e.limit),) ))
def test_one_event_event_details(self): """ Test when the calendar is empty. """ data = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:1234-5678 DTSTAMP:20080601T000000Z DTSTART:%s DTEND:%s END:VEVENT END:VCALENDAR """ % ( self.now_12H.getText(), self.now_13H.getText(), ) yield self._createCalendarObject(data, "user01", "test.ics") calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1")) fbinfo = FreebusyQuery.FBInfo([], [], []) timerange = Period(self.now, self.now_1D) event_details = [] organizer = recipient = yield calendarUserFromCalendarUserAddress( "mailto:[email protected]", self.transactionUnderTest()) freebusy = FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange, event_details=event_details) freebusy.same_calendar_user = True result = yield freebusy.generateFreeBusyInfo([ calendar, ], fbinfo) self.assertEqual(result, 1) self.assertEqual(fbinfo.busy, [ Period(self.now_12H, self.now_13H), ]) self.assertEqual(len(fbinfo.tentative), 0) self.assertEqual(len(fbinfo.unavailable), 0) self.assertEqual(len(event_details), 1) self.assertEqual( str(event_details[0]), str(tuple(Component.fromString(data).subcomponents())[0]))
def generateFreeBusyResponse(self, recipient, responses, organizerProp, uid, event_details): # Extract the ATTENDEE property matching current recipient from the calendar data cuas = recipient.record.calendarUserAddresses attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas) try: fbresult = yield FreebusyQuery( organizer=self.scheduler.organizer, organizerProp=organizerProp, recipient=recipient, attendeeProp=attendeeProp, uid=uid, timerange=self.scheduler.timeRange, excludeUID=self.scheduler.excludeUID, logItems=self.scheduler.logItems, event_details=event_details, ).generateAttendeeFreeBusyResponse() except Exception as e: log.failure( "Could not determine free busy information for recipient {cuaddr}", cuaddr=recipient.cuaddr, level=LogLevel.debug ) log.error( "Could not determine free busy information for recipient {cuaddr}: {ex}", cuaddr=recipient.cuaddr, ex=e ) err = HTTPError(ErrorResponse( responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions"), "Could not determine free busy information", )) responses.add( recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_AUTHORITY ) returnValue(False) else: responses.add( recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.SUCCESS, calendar=fbresult ) returnValue(True)
def recv_freebusy(self, txn, request): """ Process a freebusy cross-pod request. Message arguments as per L{send_freebusy}. @param request: request arguments @type request: C{dict} """ # Operate on the L{CommonHomeChild} calresource, _ignore = yield self._getStoreObjectForRequest( txn, request) organizer = yield calendarUserFromCalendarUserAddress( request["organizer"], txn) if request["organizer"] else None recipient = yield calendarUserFromCalendarUserAddress( request["recipient"], txn) if request["recipient"] else None freebusy = FreebusyQuery( organizer=organizer, recipient=recipient, timerange=Period.parseText(request["timerange"]), excludeUID=request["excludeuid"], event_details=request["event_details"], ) fbinfo = FreebusyQuery.FBInfo([], [], []) matchtotal = yield freebusy.generateFreeBusyInfo( [ calresource, ], fbinfo, matchtotal=request["matchtotal"], ) # Convert L{Period} objects to text for JSON response returnValue({ "fbresults": [ [item.getText() for item in fbinfo.busy], [item.getText() for item in fbinfo.tentative], [item.getText() for item in fbinfo.unavailable], ], "matchtotal": matchtotal, })
def test_no_events(self): """ Test when the calendar is empty. """ calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1")) fbinfo = FreebusyQuery.FBInfo([], [], []) timerange = Period(self.now, self.now_1D) organizer = recipient = yield calendarUserFromCalendarUserAddress( "mailto:[email protected]", self.transactionUnderTest()) freebusy = FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange) result = (yield freebusy.generateFreeBusyInfo([ calendar, ], fbinfo)) self.assertEqual(result, 0) self.assertEqual(len(fbinfo.busy), 0) self.assertEqual(len(fbinfo.tentative), 0) self.assertEqual(len(fbinfo.unavailable), 0)
def _processFBURL(self, request): # # Check authentication and access controls # yield self.authorize(request, (davxml.Read(),)) # Extract query parameters from the URL args = ('start', 'end', 'duration', 'token', 'format', 'user',) for arg in args: setattr(self, arg, request.args.get(arg, [None])[0]) # Some things we do not handle if self.token or self.user: raise HTTPError(ErrorResponse( responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "supported-query-parameter"), "Invalid query parameter", )) # Check format if self.format: self.format = self.format.split(";")[0] if self.format not in ("text/calendar", "text/plain"): raise HTTPError(ErrorResponse( responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "supported-format"), "Invalid return format requested", )) else: self.format = "text/calendar" # Start/end/duration must be valid iCalendar DATE-TIME UTC or DURATION values try: if self.start: self.start = DateTime.parseText(self.start) if not self.start.utc(): raise ValueError() if self.end: self.end = DateTime.parseText(self.end) if not self.end.utc(): raise ValueError() if self.duration: self.duration = Duration.parseText(self.duration) except ValueError: raise HTTPError(ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # Sanity check start/end/duration # End and duration cannot both be present if self.end and self.duration: raise HTTPError(ErrorResponse( responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # Duration must be positive if self.duration and self.duration.getTotalSeconds() < 0: raise HTTPError(ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # Now fill in the missing pieces if self.start is None: self.start = DateTime.getNowUTC() self.start.setHHMMSS(0, 0, 0) if self.duration: self.end = self.start + self.duration if self.end is None: self.end = self.start + Duration(days=config.FreeBusyURL.TimePeriod) # End > start if self.end <= self.start: raise HTTPError(ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # TODO: We should probably verify that the actual time-range is within sensible bounds (e.g. not too far in the past or future and not too long) # Now lookup the principal details for the targeted user principal = (yield self.parent.principalForRecord()) # Pick the first mailto cu address or the first other type cuaddr = None for item in principal.calendarUserAddresses(): if cuaddr is None: cuaddr = item if item.startswith("mailto:"): cuaddr = item break # Get inbox details inboxURL = principal.scheduleInboxURL() if inboxURL is None: raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox URL for principal: %s" % (principal,))) try: inbox = (yield request.locateResource(inboxURL)) except: log.error("No schedule inbox for principal: {p}", p=principal) inbox = None if inbox is None: raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox for principal: %s" % (principal,))) organizer = recipient = LocalCalendarUser(cuaddr, principal.record) recipient.inbox = inbox._newStoreObject attendeeProp = Property("ATTENDEE", recipient.cuaddr) timerange = Period(self.start, self.end) fbresult = yield FreebusyQuery( organizer=organizer, recipient=recipient, attendeeProp=attendeeProp, timerange=timerange, ).generateAttendeeFreeBusyResponse() response = Response() response.stream = MemoryStream(str(fbresult)) response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (self.format,))) returnValue(response)
def test_simple(self): data = ( ( "#1.1 No busy time", FreebusyQuery.FBInfo([], [], []), Period.parseText("20080601T000000Z/20080602T000000Z"), None, None, None, """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VFREEBUSY DTSTART:20080601T000000Z DTEND:20080602T000000Z END:VFREEBUSY END:VCALENDAR """, ), ( "#1.2 No busy time with organizerProp & attendeeProp", FreebusyQuery.FBInfo([], [], []), Period.parseText("20080601T000000Z/20080602T000000Z"), Property("ORGANIZER", "mailto:[email protected]"), Property("ATTENDEE", "mailto:[email protected]"), None, """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VFREEBUSY DTSTART:20080601T000000Z DTEND:20080602T000000Z ATTENDEE:mailto:[email protected] ORGANIZER:mailto:[email protected] END:VFREEBUSY END:VCALENDAR """, ), ( "#1.3 With single busy time", FreebusyQuery.FBInfo([ Period.parseText("20080601T120000Z/20080601T130000Z"), ], [], []), Period.parseText("20080601T000000Z/20080602T000000Z"), Property("ORGANIZER", "mailto:[email protected]"), Property("ATTENDEE", "mailto:[email protected]"), None, """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VFREEBUSY DTSTART:20080601T000000Z DTEND:20080602T000000Z ATTENDEE:mailto:[email protected] FREEBUSY;FBTYPE=BUSY:20080601T120000Z/20080601T130000Z ORGANIZER:mailto:[email protected] END:VFREEBUSY END:VCALENDAR """, ), ( "#1.4 With multiple busy time", FreebusyQuery.FBInfo( [ Period.parseText("20080601T120000Z/20080601T130000Z"), Period.parseText("20080601T140000Z/20080601T150000Z"), ], [], [], ), Period.parseText("20080601T000000Z/20080602T000000Z"), Property("ORGANIZER", "mailto:[email protected]"), Property("ATTENDEE", "mailto:[email protected]"), None, """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VFREEBUSY DTSTART:20080601T000000Z DTEND:20080602T000000Z ATTENDEE:mailto:[email protected] FREEBUSY;FBTYPE=BUSY:20080601T120000Z/20080601T130000Z,20080601T140000Z/20080601T150000Z ORGANIZER:mailto:[email protected] END:VFREEBUSY END:VCALENDAR """, ), ( "#1.5 With multiple busy time, some overlap", FreebusyQuery.FBInfo( [ Period.parseText("20080601T120000Z/20080601T130000Z"), Period.parseText("20080601T123000Z/20080601T133000Z"), Period.parseText("20080601T140000Z/20080601T150000Z"), Period.parseText("20080601T150000Z/20080601T160000Z"), ], [], [], ), Period.parseText("20080601T000000Z/20080602T000000Z"), Property("ORGANIZER", "mailto:[email protected]"), Property("ATTENDEE", "mailto:[email protected]"), None, """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VFREEBUSY DTSTART:20080601T000000Z DTEND:20080602T000000Z ATTENDEE:mailto:[email protected] FREEBUSY;FBTYPE=BUSY:20080601T120000Z/20080601T133000Z,20080601T140000Z/20080601T160000Z ORGANIZER:mailto:[email protected] END:VFREEBUSY END:VCALENDAR """, ), ( "#1.6 With all busy time types", FreebusyQuery.FBInfo( [ Period.parseText("20080601T120000Z/20080601T130000Z"), Period.parseText("20080601T140000Z/20080601T150000Z"), ], [ Period.parseText("20080601T140000Z/20080601T150000Z"), ], [ Period.parseText("20080601T160000Z/20080601T170000Z"), ], ), Period.parseText("20080601T000000Z/20080602T000000Z"), Property("ORGANIZER", "mailto:[email protected]"), Property("ATTENDEE", "mailto:[email protected]"), None, """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VFREEBUSY DTSTART:20080601T000000Z DTEND:20080602T000000Z ATTENDEE:mailto:[email protected] FREEBUSY;FBTYPE=BUSY:20080601T120000Z/20080601T130000Z,20080601T140000Z/20080601T150000Z FREEBUSY;FBTYPE=BUSY-TENTATIVE:20080601T140000Z/20080601T150000Z FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20080601T160000Z/20080601T170000Z ORGANIZER:mailto:[email protected] END:VFREEBUSY END:VCALENDAR """, ), ( "#1.7 With single busy time and event details", FreebusyQuery.FBInfo( [ Period.parseText("20080601T120000Z/20080601T130000Z"), ], [], [], ), Period.parseText("20080601T000000Z/20080602T000000Z"), Property("ORGANIZER", "mailto:[email protected]"), Property("ATTENDEE", "mailto:[email protected]"), [ tuple( Component.fromString("""BEGIN:VCALENDAR VERSION:2.0 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:1234-5678 DTSTAMP:20080601T000000Z DTSTART:20080601T120000Z DTEND:20080601T130000Z END:VEVENT END:VCALENDAR """).subcomponents())[0], ], """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT DTSTART:20080601T120000Z DTEND:20080601T130000Z END:VEVENT BEGIN:VFREEBUSY DTSTART:20080601T000000Z DTEND:20080602T000000Z ATTENDEE:mailto:[email protected] FREEBUSY;FBTYPE=BUSY:20080601T120000Z/20080601T130000Z ORGANIZER:mailto:[email protected] END:VFREEBUSY END:VCALENDAR """, ), ) for description, fbinfo, timerange, organizerProp, attendeeProp, event_details, calendar in data: freebusy = FreebusyQuery(organizerProp=organizerProp, attendeeProp=attendeeProp, timerange=timerange, event_details=event_details) result = freebusy.buildFreeBusyResult(fbinfo) self.assertEqual(normalizeiCalendarText(str(result)), calendar.replace("\n", "\r\n"), msg=description)