def eventsInTimeRange(self, calendar, uid, timerange): # Create fake filter element to match time-range filter = caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( timerange, name=("VEVENT", ), ), name="VCALENDAR", )) filter = Filter(filter) filter.settimezone(None) matches = yield calendar.search(filter, useruid=uid, fbtype=False) if matches is None: returnValue(None) for name, _ignore_uid, _ignore_type in matches: event = yield calendar.calendarObjectWithName(name) ical_data = yield event.componentForUser() ical_data.stripStandardTimezones() table = tables.Table() table.addRow(( "Calendar:", calendar.name(), )) table.addRow(("Resource Name:", name)) table.addRow(("Resource ID:", event._resourceID)) table.addRow(("Created", event.created())) table.addRow(("Modified", event.modified())) print("\n") table.printTable() print(ical_data.getTextWithoutTimezones())
def eventsInTimeRange(self, calendar, uid, timerange): # Create fake filter element to match time-range filter = caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( timerange, name=("VEVENT",), ), name="VCALENDAR", ) ) filter = Filter(filter) filter.settimezone(None) matches = yield calendar.search(filter, useruid=uid, fbtype=False) if matches is None: returnValue(None) for name, _ignore_uid, _ignore_type in matches: event = yield calendar.calendarObjectWithName(name) ical_data = yield event.componentForUser() ical_data.stripStandardTimezones() table = tables.Table() table.addRow(("Calendar:", calendar.name(),)) table.addRow(("Resource Name:", name)) table.addRow(("Resource ID:", event._resourceID)) table.addRow(("Created", event.created())) table.addRow(("Modified", event.modified())) print("\n") table.printTable() print(ical_data.getTextWithoutTimezones())
def report_urn_ietf_params_xml_ns_caldav_calendar_query(self, request, calendar_query): """ Generate a calendar-query REPORT. (CalDAV-access-09, section 7.6) """ # Verify root element if calendar_query.qname() != (caldav_namespace, "calendar-query"): raise ValueError("{CalDAV:}calendar-query expected as root element, not %s." % (calendar_query.sname(),)) if not self.isCollection(): parent = (yield self.locateParent(request, request.uri)) if not parent.isPseudoCalendarCollection(): log.error("calendar-query report is not allowed on a resource outside of a calendar collection %s" % (self,)) raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Must be calendar collection or calendar resource")) responses = [] xmlfilter = calendar_query.filter filter = Filter(xmlfilter) props = calendar_query.props assert props is not None # Get the original timezone provided in the query, if any, and validate it now query_timezone = None query_tz = calendar_query.timezone if query_tz is not None and not query_tz.valid(): msg = "CalDAV:timezone must contain one VTIMEZONE component only: %s" % (query_tz,) log.error(msg) raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), "Invalid calendar-data", )) if query_tz: filter.settimezone(query_tz) query_timezone = tuple(calendar_query.timezone.calendar().subcomponents())[0] if props.qname() == ("DAV:", "allprop"): propertiesForResource = report_common.allPropertiesForResource generate_calendar_data = False elif props.qname() == ("DAV:", "propname"): propertiesForResource = report_common.propertyNamesForResource generate_calendar_data = False elif props.qname() == ("DAV:", "prop"): propertiesForResource = report_common.propertyListForResource # Verify that any calendar-data element matches what we can handle result, message, generate_calendar_data = report_common.validPropertyListCalendarDataTypeVersion(props) if not result: log.error(message) raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data"), "Invalid calendar-data", )) else: raise AssertionError("We shouldn't be here") # Verify that the filter element is valid if (filter is None) or not filter.valid(): log.error("Invalid filter element: %r" % (xmlfilter,)) raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, (caldav_namespace, "valid-filter"), "Invalid filter element", )) matchcount = [0] max_number_of_results = [config.MaxQueryWithDataResults if generate_calendar_data else None, ] @inlineCallbacks def doQuery(calresource, uri): """ Run a query on the specified calendar collection accumulating the query responses. @param calresource: the L{CalDAVResource} for a calendar collection. @param uri: the uri for the calendar collection resource. """ @inlineCallbacks def queryCalendarObjectResource(resource, uri, name, calendar, timezone, query_ok=False, isowner=True): """ Run a query on the specified calendar. @param resource: the L{CalDAVResource} for the calendar. @param uri: the uri of the resource. @param name: the name of the resource. @param calendar: the L{Component} calendar read from the resource. """ # Handle private events access restrictions if not isowner: access = resource.accessMode else: access = None if query_ok or filter.match(calendar, access): # Check size of results is within limit matchcount[0] += 1 if max_number_of_results[0] is not None and matchcount[0] > max_number_of_results[0]: raise NumberOfMatchesWithinLimits(max_number_of_results[0]) if name: href = davxml.HRef.fromString(joinURL(uri, name)) else: href = davxml.HRef.fromString(uri) try: yield report_common.responseForHref(request, responses, href, resource, propertiesForResource, props, isowner, calendar=calendar, timezone=timezone) except ConcurrentModification: # This can happen because of a race-condition between the # time we determine which resources exist and the deletion # of one of these resources in another request. In this # case, we ignore the now missing resource rather # than raise an error for the entire report. log.error("Missing resource during query: %s" % (href,)) # Check whether supplied resource is a calendar or a calendar object resource if calresource.isPseudoCalendarCollection(): # Get the timezone property from the collection if one was not set in the query, # and store in the query filter for later use has_prop = (yield calresource.hasProperty(CalendarTimeZone(), request)) timezone = query_timezone if query_tz is None and has_prop: tz = (yield calresource.readProperty(CalendarTimeZone(), request)) filter.settimezone(tz) timezone = tuple(tz.calendar().subcomponents())[0] # Do some optimization of access control calculation by determining any inherited ACLs outside of # the child resource loop and supply those to the checkPrivileges on each child. filteredaces = (yield calresource.inheritedACEsforChildren(request)) # Check private events access status isowner = (yield calresource.isOwner(request)) # Check for disabled access if filteredaces is not None: index_query_ok = True try: # Get list of children that match the search and have read access names = [name for name, ignore_uid, ignore_type in (yield calresource.search(filter))] except IndexedSearchException: names = yield calresource.listChildren() index_query_ok = False if not names: returnValue(True) # Now determine which valid resources are readable and which are not ok_resources = [] yield calresource.findChildrenFaster( "1", request, lambda x, y: ok_resources.append((x, y)), None, None, None, names, (davxml.Read(),), inherited_aces=filteredaces ) for child, child_uri in ok_resources: child_uri_name = child_uri[child_uri.rfind("/") + 1:] if generate_calendar_data or not index_query_ok: calendar = (yield child.iCalendarForUser(request)) assert calendar is not None, "Calendar %s is missing from calendar collection %r" % (child_uri_name, self) else: calendar = None yield queryCalendarObjectResource(child, uri, child_uri_name, calendar, timezone, query_ok=index_query_ok, isowner=isowner) else: # Get the timezone property from the collection if one was not set in the query, # and store in the query object for later use timezone = query_timezone if query_tz is None: parent = (yield calresource.locateParent(request, uri)) assert parent is not None and parent.isPseudoCalendarCollection() has_prop = (yield parent.hasProperty(CalendarTimeZone(), request)) if has_prop: tz = (yield parent.readProperty(CalendarTimeZone(), request)) filter.settimezone(tz) timezone = tuple(tz.calendar().subcomponents())[0] # Check private events access status isowner = (yield calresource.isOwner(request)) calendar = (yield calresource.iCalendarForUser(request)) yield queryCalendarObjectResource(calresource, uri, None, calendar, timezone) returnValue(True) # Run report taking depth into account try: depth = request.headers.getHeader("depth", "0") yield report_common.applyToCalendarCollections(self, request, request.uri, depth, doQuery, (davxml.Read(),)) except TooManyInstancesError, ex: log.error("Too many instances need to be computed in calendar-query report") raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, MaxInstances.fromString(str(ex.max_allowed)), "Too many instances", ))
def _matchCalendarResources(self, calresource): # Get the timezone property from the collection. tz = calresource.getTimezone() # Try cache aggregated_resources = (yield FBCacheEntry.getCacheEntry(calresource, self.attendee_uid, self.timerange)) if config.EnableFreeBusyCache else None if aggregated_resources is None: if self.accountingItems is not None: self.accountingItems["fb-uncached"] = self.accountingItems.get("fb-uncached", 0) + 1 caching = False if config.EnableFreeBusyCache: # Log extended item if self.logItems is not None: self.logItems["fb-uncached"] = self.logItems.get("fb-uncached", 0) + 1 # We want to cache a large range of time based on the current date cache_start = normalizeToUTC(DateTime.getToday() + Duration(days=0 - config.FreeBusyCacheDaysBack)) cache_end = normalizeToUTC(DateTime.getToday() + Duration(days=config.FreeBusyCacheDaysForward)) # If the requested time range would fit in our allowed cache range, trigger the cache creation if compareDateTime(self.timerange.getStart(), cache_start) >= 0 and compareDateTime(self.timerange.getEnd(), cache_end) <= 0: cache_timerange = Period(cache_start, cache_end) caching = True # # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range. # We then take those results and merge them into one VFREEBUSY component # with appropriate FREEBUSY properties, and return that single item as iCal data. # # Create fake filter element to match time-range tr = TimeRange( start=(cache_timerange if caching else self.timerange).getStart().getText(), end=(cache_timerange if caching else self.timerange).getEnd().getText(), ) filter = caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( tr, name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"), ), name="VCALENDAR", ) ) filter = Filter(filter) tzinfo = filter.settimezone(tz) if self.accountingItems is not None: self.accountingItems["fb-query-timerange"] = (str(tr.start), str(tr.end),) try: resources = yield calresource.search(filter, useruid=self.attendee_uid, fbtype=True) aggregated_resources = {} for name, uid, comptype, test_organizer, float, start, end, fbtype, transp in resources: if transp == 'T' and fbtype != '?': fbtype = 'F' aggregated_resources.setdefault((name, uid, comptype, test_organizer,), []).append(( float, tupleFromDateTime(parseSQLTimestampToPyCalendar(start)), tupleFromDateTime(parseSQLTimestampToPyCalendar(end)), fbtype, )) if caching: yield FBCacheEntry.makeCacheEntry(calresource, self.attendee_uid, cache_timerange, aggregated_resources) except IndexedSearchException: raise InternalDataStoreError("Invalid indexedSearch query") else: if self.accountingItems is not None: self.accountingItems["fb-cached"] = self.accountingItems.get("fb-cached", 0) + 1 # Log extended item if self.logItems is not None: self.logItems["fb-cached"] = self.logItems.get("fb-cached", 0) + 1 # Determine appropriate timezone (UTC is the default) tzinfo = tz.gettimezone() if tz is not None else Timezone.UTCTimezone filter = None returnValue((aggregated_resources, tzinfo, filter,))
def _internalGenerateFreeBusyInfo( calresource, fbinfo, timerange, matchtotal, excludeuid=None, organizer=None, organizerPrincipal=None, same_calendar_user=False, servertoserver=False, event_details=None, logItems=None, accountingItems=None, ): """ Run a free busy report on the specified calendar collection accumulating the free busy info for later processing. @param calresource: the L{Calendar} for a calendar collection. @param fbinfo: the array of busy periods to update. @param timerange: the L{TimeRange} for the query. @param matchtotal: the running total for the number of matches. @param excludeuid: a C{str} containing a UID value to exclude any components with that UID from contributing to free-busy. @param organizer: a C{str} containing the value of the ORGANIZER property in the VFREEBUSY request. This is used in conjunction with the UID value to process exclusions. @param same_calendar_user: a C{bool} indicating whether the calendar user requesting the free-busy information is the same as the calendar user being targeted. @param servertoserver: a C{bool} indicating whether we are doing a local or remote lookup request. @param event_details: a C{list} into which to store extended VEVENT details if not C{None} @param logItems: a C{dict} to store logging info to @param accountingItems: a C{dict} to store accounting info to """ # First check the privilege on this collection # TODO: for server-to-server we bypass this right now as we have no way to authorize external users. # TODO: actually we by pass altogether by assuming anyone can check anyone else's freebusy # May need organizer principal organizer_record = (yield calresource.directoryService( ).recordWithCalendarUserAddress(organizer)) if organizer else None organizer_uid = organizer_record.uid if organizer_record else "" # Free busy is per-user attendee_uid = calresource.viewerHome().uid() attendee_record = yield calresource.directoryService().recordWithUID( attendee_uid.decode("utf-8")) # Get the timezone property from the collection. tz = calresource.getTimezone() # Look for possible extended free busy information rich_options = { "organizer": False, "delegate": False, "resource": False, } do_event_details = False if event_details is not None and organizer_record is not None and attendee_record is not None: # Get the principal of the authorized user which may be different from the organizer if a delegate of # the organizer is making the request authz_uid = organizer_uid authz_record = organizer_record if calresource._txn._authz_uid is not None and calresource._txn._authz_uid != organizer_uid: authz_uid = calresource._txn._authz_uid authz_record = yield calresource.directoryService().recordWithUID( authz_uid.decode("utf-8")) # Check if attendee is also the organizer or the delegate doing the request if attendee_uid in (organizer_uid, authz_uid): do_event_details = True rich_options["organizer"] = True # Check if authorized user is a delegate of attendee proxy = (yield authz_record.isProxyFor(attendee_record)) if config.Scheduling.Options.DelegeteRichFreeBusy and proxy: do_event_details = True rich_options["delegate"] = True # Check if attendee is room or resource if config.Scheduling.Options.RoomResourceRichFreeBusy and attendee_record.getCUType( ) in ( "RESOURCE", "ROOM", ): do_event_details = True rich_options["resource"] = True # Try cache resources = (yield FBCacheEntry.getCacheEntry( calresource, attendee_uid, timerange)) if config.EnableFreeBusyCache else None if resources is None: if accountingItems is not None: accountingItems["fb-uncached"] = accountingItems.get( "fb-uncached", 0) + 1 caching = False if config.EnableFreeBusyCache: # Log extended item if logItems is not None: logItems["fb-uncached"] = logItems.get("fb-uncached", 0) + 1 # We want to cache a large range of time based on the current date cache_start = normalizeToUTC(DateTime.getToday() + Duration( days=0 - config.FreeBusyCacheDaysBack)) cache_end = normalizeToUTC(DateTime.getToday() + Duration( days=config.FreeBusyCacheDaysForward)) # If the requested time range would fit in our allowed cache range, trigger the cache creation if compareDateTime(timerange.start, cache_start) >= 0 and compareDateTime( timerange.end, cache_end) <= 0: cache_timerange = TimeRange(start=cache_start.getText(), end=cache_end.getText()) caching = True # # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range. # We then take those results and merge them into one VFREEBUSY component # with appropriate FREEBUSY properties, and return that single item as iCal data. # # Create fake filter element to match time-range filter = caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( cache_timerange if caching else timerange, name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"), ), name="VCALENDAR", )) filter = Filter(filter) tzinfo = filter.settimezone(tz) if accountingItems is not None: tr = cache_timerange if caching else timerange accountingItems["fb-query-timerange"] = ( str(tr.start), str(tr.end), ) try: resources = yield calresource.search(filter, useruid=attendee_uid, fbtype=True) if caching: yield FBCacheEntry.makeCacheEntry(calresource, attendee_uid, cache_timerange, resources) except IndexedSearchException: raise InternalDataStoreError("Invalid indexedSearch query") else: if accountingItems is not None: accountingItems["fb-cached"] = accountingItems.get("fb-cached", 0) + 1 # Log extended item if logItems is not None: logItems["fb-cached"] = logItems.get("fb-cached", 0) + 1 # Determine appropriate timezone (UTC is the default) tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True) # We care about separate instances for VEVENTs only aggregated_resources = {} for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources: if transp == 'T' and fbtype != '?': fbtype = 'F' aggregated_resources.setdefault(( name, uid, type, test_organizer, ), []).append(( float, start, end, fbtype, )) if accountingItems is not None: accountingItems["fb-resources"] = {} for k, v in aggregated_resources.items(): name, uid, type, test_organizer = k accountingItems["fb-resources"][uid] = [] for float, start, end, fbtype in v: fbstart = parseSQLTimestampToPyCalendar(start) if float == 'Y': fbstart.setTimezone(tzinfo) else: fbstart.setTimezone(Timezone(utc=True)) fbend = parseSQLTimestampToPyCalendar(end) if float == 'Y': fbend.setTimezone(tzinfo) else: fbend.setTimezone(Timezone(utc=True)) accountingItems["fb-resources"][uid].append(( float, str(fbstart), str(fbend), fbtype, )) # Cache directory record lookup outside this loop as it is expensive and will likely # always end up being called with the same organizer address. recordUIDCache = {} for key in aggregated_resources.iterkeys(): name, uid, type, test_organizer = key # Short-cut - if an fbtype exists we can use that if type == "VEVENT" and aggregated_resources[key][0][3] != '?': matchedResource = False # Look at each instance for float, start, end, fbtype in aggregated_resources[key]: # Ignore free time or unknown if fbtype in ('F', '?'): continue # Ignore ones of this UID if excludeuid: # See if we have a UID match if (excludeuid == uid): if test_organizer: test_uid = recordUIDCache.get(test_organizer) if test_uid is None: test_record = (yield calresource.directoryService( ).recordWithCalendarUserAddress( test_organizer)) test_uid = test_record.uid if test_record else "" recordUIDCache[test_organizer] = test_uid else: test_uid = "" # Check that ORGANIZER's match (security requirement) if (organizer is None) or (organizer_uid == test_uid): continue # Check for no ORGANIZER and check by same calendar user elif (test_uid == "") and same_calendar_user: continue # Apply a timezone to any floating times fbstart = parseSQLTimestampToPyCalendar(start) if float == 'Y': fbstart.setTimezone(tzinfo) else: fbstart.setTimezone(Timezone(utc=True)) fbend = parseSQLTimestampToPyCalendar(end) if float == 'Y': fbend.setTimezone(tzinfo) else: fbend.setTimezone(Timezone(utc=True)) # Clip instance to time range clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), Period(timerange.start, timerange.end)) # Double check for overlap if clipped: matchedResource = True fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped) if matchedResource: # Check size of results is within limit matchtotal += 1 if matchtotal > config.MaxQueryWithDataResults: raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal) # Add extended details if do_event_details: child = (yield calresource.calendarObjectWithName(name)) # Only add fully public events if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC: calendar = (yield child.componentForUser()) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) else: child = (yield calresource.calendarObjectWithName(name)) calendar = (yield child.componentForUser()) # The calendar may come back as None if the resource is being changed, or was deleted # between our initial index query and getting here. For now we will ignore this error, but in # the longer term we need to implement some form of locking, perhaps. if calendar is None: log.error( "Calendar %s is missing from calendar collection %r" % (name, calresource)) continue # Ignore ones of this UID if excludeuid: # See if we have a UID match if (excludeuid == uid): test_organizer = calendar.getOrganizer() if test_organizer: test_uid = recordUIDCache.get(test_organizer) if test_uid is None: test_record = (yield calresource.directoryService( ).recordWithCalendarUserAddress(test_organizer)) test_uid = test_record.uid if test_record else "" recordUIDCache[test_organizer] = test_uid else: test_uid = "" # Check that ORGANIZER's match (security requirement) if (organizer is None) or (organizer_uid == test_uid): continue # Check for no ORGANIZER and check by same calendar user elif (test_organizer is None) and same_calendar_user: continue if accountingItems is not None: accountingItems.setdefault("fb-filter-match", []).append(uid) if filter.match(calendar, None): if accountingItems is not None: accountingItems.setdefault("fb-filter-matched", []).append(uid) # Check size of results is within limit matchtotal += 1 if matchtotal > config.MaxQueryWithDataResults: raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal) if calendar.mainType() == "VEVENT": processEventFreeBusy(calendar, fbinfo, timerange, tzinfo) elif calendar.mainType() == "VFREEBUSY": processFreeBusyFreeBusy(calendar, fbinfo, timerange) elif calendar.mainType() == "VAVAILABILITY": processAvailabilityFreeBusy(calendar, fbinfo, timerange) else: assert "Free-busy query returned unwanted component: %s in %r", ( name, calresource, ) # Add extended details if calendar.mainType() == "VEVENT" and do_event_details: child = (yield calresource.calendarObjectWithName(name)) # Only add fully public events if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC: calendar = (yield child.componentForUser()) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) returnValue(matchtotal)
def generateFreeBusyInfo( request, calresource, fbinfo, timerange, matchtotal, excludeuid=None, organizer=None, organizerPrincipal=None, same_calendar_user=False, servertoserver=False, event_details=None, ): """ Run a free busy report on the specified calendar collection accumulating the free busy info for later processing. @param request: the L{IRequest} for the current request. @param calresource: the L{CalDAVResource} for a calendar collection. @param fbinfo: the array of busy periods to update. @param timerange: the L{TimeRange} for the query. @param matchtotal: the running total for the number of matches. @param excludeuid: a C{str} containing a UID value to exclude any components with that UID from contributing to free-busy. @param organizer: a C{str} containing the value of the ORGANIZER property in the VFREEBUSY request. This is used in conjunction with the UID value to process exclusions. @param same_calendar_user: a C{bool} indicating whether the calendar user requesting the free-busy information is the same as the calendar user being targeted. @param servertoserver: a C{bool} indicating whether we are doing a local or remote lookup request. @param event_details: a C{list} into which to store extended VEVENT details if not C{None} """ # First check the privilege on this collection # TODO: for server-to-server we bypass this right now as we have no way to authorize external users. if not servertoserver: try: yield calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), principal=organizerPrincipal) except AccessDeniedError: returnValue(matchtotal) # May need organizer principal organizer_principal = (yield calresource.principalForCalendarUserAddress(organizer)) if organizer else None organizer_uid = organizer_principal.principalUID() if organizer_principal else "" # Free busy is per-user userPrincipal = (yield calresource.resourceOwnerPrincipal(request)) if userPrincipal: useruid = userPrincipal.principalUID() else: useruid = "" # Get the timezone property from the collection. has_prop = (yield calresource.hasProperty(CalendarTimeZone(), request)) if has_prop: tz = (yield calresource.readProperty(CalendarTimeZone(), request)) else: tz = None # Look for possible extended free busy information rich_options = { "organizer": False, "delegate": False, "resource": False, } do_event_details = False if event_details is not None and organizer_principal is not None and userPrincipal is not None: # Check if organizer is attendee if organizer_principal == userPrincipal: do_event_details = True rich_options["organizer"] = True # Check if organizer is a delegate of attendee proxy = (yield organizer_principal.isProxyFor(userPrincipal)) if config.Scheduling.Options.DelegeteRichFreeBusy and proxy: do_event_details = True rich_options["delegate"] = True # Check if attendee is room or resource if config.Scheduling.Options.RoomResourceRichFreeBusy and userPrincipal.getCUType() in ("RESOURCE", "ROOM",): do_event_details = True rich_options["resource"] = True # Try cache resources = (yield FBCacheEntry.getCacheEntry(calresource, useruid, timerange)) if config.EnableFreeBusyCache else None if resources is None: caching = False if config.EnableFreeBusyCache: # Log extended item if not hasattr(request, "extendedLogItems"): request.extendedLogItems = {} request.extendedLogItems["fb-uncached"] = request.extendedLogItems.get("fb-uncached", 0) + 1 # We want to cache a large range of time based on the current date cache_start = normalizeToUTC(DateTime.getToday() + Duration(days=0 - config.FreeBusyCacheDaysBack)) cache_end = normalizeToUTC(DateTime.getToday() + Duration(days=config.FreeBusyCacheDaysForward)) # If the requested timerange would fit in our allowed cache range, trigger the cache creation if compareDateTime(timerange.start, cache_start) >= 0 and compareDateTime(timerange.end, cache_end) <= 0: cache_timerange = TimeRange(start=cache_start.getText(), end=cache_end.getText()) caching = True # # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range. # We then take those results and merge them into one VFREEBUSY component # with appropriate FREEBUSY properties, and return that single item as iCal data. # # Create fake filter element to match time-range filter = caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( cache_timerange if caching else timerange, name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"), ), name="VCALENDAR", ) ) filter = Filter(filter) tzinfo = filter.settimezone(tz) try: resources = yield calresource.search(filter, useruid=useruid, fbtype=True) if caching: yield FBCacheEntry.makeCacheEntry(calresource, useruid, cache_timerange, resources) except IndexedSearchException: raise HTTPError(StatusResponse( responsecode.INTERNAL_SERVER_ERROR, "Failed freebusy query" )) else: # Log extended item if not hasattr(request, "extendedLogItems"): request.extendedLogItems = {} request.extendedLogItems["fb-cached"] = request.extendedLogItems.get("fb-cached", 0) + 1 # Determine appropriate timezone (UTC is the default) tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True) # We care about separate instances for VEVENTs only aggregated_resources = {} for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources: if transp == 'T' and fbtype != '?': fbtype = 'F' aggregated_resources.setdefault((name, uid, type, test_organizer,), []).append((float, start, end, fbtype,)) for key in aggregated_resources.iterkeys(): name, uid, type, test_organizer = key # Short-cut - if an fbtype exists we can use that if type == "VEVENT" and aggregated_resources[key][0][3] != '?': matchedResource = False # Look at each instance for float, start, end, fbtype in aggregated_resources[key]: # Ignore free time or unknown if fbtype in ('F', '?'): continue # Ignore ones of this UID if excludeuid: # See if we have a UID match if (excludeuid == uid): test_principal = (yield calresource.principalForCalendarUserAddress(test_organizer)) if test_organizer else None test_uid = test_principal.principalUID() if test_principal else "" # Check that ORGANIZER's match (security requirement) if (organizer is None) or (organizer_uid == test_uid): continue # Check for no ORGANIZER and check by same calendar user elif (test_uid == "") and same_calendar_user: continue # Apply a timezone to any floating times fbstart = parseSQLTimestampToPyCalendar(start) if float == 'Y': fbstart.setTimezone(tzinfo) else: fbstart.setTimezone(Timezone(utc=True)) fbend = parseSQLTimestampToPyCalendar(end) if float == 'Y': fbend.setTimezone(tzinfo) else: fbend.setTimezone(Timezone(utc=True)) # Clip instance to time range clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), Period(timerange.start, timerange.end)) # Double check for overlap if clipped: matchedResource = True fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped) if matchedResource: # Check size of results is within limit matchtotal += 1 if matchtotal > max_number_of_matches: raise NumberOfMatchesWithinLimits(max_number_of_matches) # Add extended details if do_event_details: child = (yield request.locateChildResource(calresource, name)) calendar = (yield child.iCalendarForUser(request)) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) else: child = (yield request.locateChildResource(calresource, name)) calendar = (yield child.iCalendarForUser(request)) # The calendar may come back as None if the resource is being changed, or was deleted # between our initial index query and getting here. For now we will ignore this error, but in # the longer term we need to implement some form of locking, perhaps. if calendar is None: log.error("Calendar %s is missing from calendar collection %r" % (name, calresource)) continue # Ignore ones of this UID if excludeuid: # See if we have a UID match if (excludeuid == uid): test_organizer = calendar.getOrganizer() test_principal = (yield calresource.principalForCalendarUserAddress(test_organizer)) if test_organizer else None test_uid = test_principal.principalUID() if test_principal else "" # Check that ORGANIZER's match (security requirement) if (organizer is None) or (organizer_uid == test_uid): continue # Check for no ORGANIZER and check by same calendar user elif (test_organizer is None) and same_calendar_user: continue if filter.match(calendar, None): # Check size of results is within limit matchtotal += 1 if matchtotal > max_number_of_matches: raise NumberOfMatchesWithinLimits(max_number_of_matches) if calendar.mainType() == "VEVENT": processEventFreeBusy(calendar, fbinfo, timerange, tzinfo) elif calendar.mainType() == "VFREEBUSY": processFreeBusyFreeBusy(calendar, fbinfo, timerange) elif calendar.mainType() == "VAVAILABILITY": processAvailabilityFreeBusy(calendar, fbinfo, timerange) else: assert "Free-busy query returned unwanted component: %s in %r", (name, calresource,) # Add extended details if calendar.mainType() == "VEVENT" and do_event_details: child = (yield request.locateChildResource(calresource, name)) calendar = (yield child.iCalendarForUser(request)) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) returnValue(matchtotal)
def generateFreeBusyInfo( request, calresource, fbinfo, timerange, matchtotal, excludeuid=None, organizer=None, organizerPrincipal=None, same_calendar_user=False, servertoserver=False, event_details=None, ): """ Run a free busy report on the specified calendar collection accumulating the free busy info for later processing. @param request: the L{IRequest} for the current request. @param calresource: the L{CalDAVResource} for a calendar collection. @param fbinfo: the array of busy periods to update. @param timerange: the L{TimeRange} for the query. @param matchtotal: the running total for the number of matches. @param excludeuid: a C{str} containing a UID value to exclude any components with that UID from contributing to free-busy. @param organizer: a C{str} containing the value of the ORGANIZER property in the VFREEBUSY request. This is used in conjunction with the UID value to process exclusions. @param same_calendar_user: a C{bool} indicating whether the calendar user requesting the free-busy information is the same as the calendar user being targeted. @param servertoserver: a C{bool} indicating whether we are doing a local or remote lookup request. @param event_details: a C{list} into which to store extended VEVENT details if not C{None} """ # First check the privilege on this collection # TODO: for server-to-server we bypass this right now as we have no way to authorize external users. if not servertoserver: try: yield calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(), ), principal=organizerPrincipal) except AccessDeniedError: returnValue(matchtotal) # May need organizer principal organizer_principal = (yield calresource.principalForCalendarUserAddress( organizer)) if organizer else None organizer_uid = organizer_principal.principalUID( ) if organizer_principal else "" # Free busy is per-user userPrincipal = (yield calresource.resourceOwnerPrincipal(request)) if userPrincipal: useruid = userPrincipal.principalUID() else: useruid = "" # Get the timezone property from the collection. has_prop = (yield calresource.hasProperty(CalendarTimeZone(), request)) if has_prop: tz = (yield calresource.readProperty(CalendarTimeZone(), request)) else: tz = None # Look for possible extended free busy information rich_options = { "organizer": False, "delegate": False, "resource": False, } do_event_details = False if event_details is not None and organizer_principal is not None and userPrincipal is not None: # Check if organizer is attendee if organizer_principal == userPrincipal: do_event_details = True rich_options["organizer"] = True # Check if organizer is a delegate of attendee proxy = (yield organizer_principal.isProxyFor(userPrincipal)) if config.Scheduling.Options.DelegeteRichFreeBusy and proxy: do_event_details = True rich_options["delegate"] = True # Check if attendee is room or resource if config.Scheduling.Options.RoomResourceRichFreeBusy and userPrincipal.getCUType( ) in ( "RESOURCE", "ROOM", ): do_event_details = True rich_options["resource"] = True # Try cache resources = (yield FBCacheEntry.getCacheEntry( calresource, useruid, timerange)) if config.EnableFreeBusyCache else None if resources is None: caching = False if config.EnableFreeBusyCache: # Log extended item if not hasattr(request, "extendedLogItems"): request.extendedLogItems = {} request.extendedLogItems[ "fb-uncached"] = request.extendedLogItems.get( "fb-uncached", 0) + 1 # We want to cache a large range of time based on the current date cache_start = normalizeToUTC(DateTime.getToday() + Duration( days=0 - config.FreeBusyCacheDaysBack)) cache_end = normalizeToUTC(DateTime.getToday() + Duration( days=config.FreeBusyCacheDaysForward)) # If the requested timerange would fit in our allowed cache range, trigger the cache creation if compareDateTime(timerange.start, cache_start) >= 0 and compareDateTime( timerange.end, cache_end) <= 0: cache_timerange = TimeRange(start=cache_start.getText(), end=cache_end.getText()) caching = True # # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range. # We then take those results and merge them into one VFREEBUSY component # with appropriate FREEBUSY properties, and return that single item as iCal data. # # Create fake filter element to match time-range filter = caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( cache_timerange if caching else timerange, name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"), ), name="VCALENDAR", )) filter = Filter(filter) tzinfo = filter.settimezone(tz) try: resources = yield calresource.search(filter, useruid=useruid, fbtype=True) if caching: yield FBCacheEntry.makeCacheEntry(calresource, useruid, cache_timerange, resources) except IndexedSearchException: raise HTTPError( StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "Failed freebusy query")) else: # Log extended item if not hasattr(request, "extendedLogItems"): request.extendedLogItems = {} request.extendedLogItems["fb-cached"] = request.extendedLogItems.get( "fb-cached", 0) + 1 # Determine appropriate timezone (UTC is the default) tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True) # We care about separate instances for VEVENTs only aggregated_resources = {} for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources: if transp == 'T' and fbtype != '?': fbtype = 'F' aggregated_resources.setdefault(( name, uid, type, test_organizer, ), []).append(( float, start, end, fbtype, )) for key in aggregated_resources.iterkeys(): name, uid, type, test_organizer = key # Short-cut - if an fbtype exists we can use that if type == "VEVENT" and aggregated_resources[key][0][3] != '?': matchedResource = False # Look at each instance for float, start, end, fbtype in aggregated_resources[key]: # Ignore free time or unknown if fbtype in ('F', '?'): continue # Ignore ones of this UID if excludeuid: # See if we have a UID match if (excludeuid == uid): test_principal = ( yield calresource.principalForCalendarUserAddress( test_organizer)) if test_organizer else None test_uid = test_principal.principalUID( ) if test_principal else "" # Check that ORGANIZER's match (security requirement) if (organizer is None) or (organizer_uid == test_uid): continue # Check for no ORGANIZER and check by same calendar user elif (test_uid == "") and same_calendar_user: continue # Apply a timezone to any floating times fbstart = parseSQLTimestampToPyCalendar(start) if float == 'Y': fbstart.setTimezone(tzinfo) else: fbstart.setTimezone(Timezone(utc=True)) fbend = parseSQLTimestampToPyCalendar(end) if float == 'Y': fbend.setTimezone(tzinfo) else: fbend.setTimezone(Timezone(utc=True)) # Clip instance to time range clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), Period(timerange.start, timerange.end)) # Double check for overlap if clipped: matchedResource = True fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped) if matchedResource: # Check size of results is within limit matchtotal += 1 if matchtotal > max_number_of_matches: raise NumberOfMatchesWithinLimits(max_number_of_matches) # Add extended details if do_event_details: child = (yield request.locateChildResource(calresource, name)) calendar = (yield child.iCalendarForUser(request)) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) else: child = (yield request.locateChildResource(calresource, name)) calendar = (yield child.iCalendarForUser(request)) # The calendar may come back as None if the resource is being changed, or was deleted # between our initial index query and getting here. For now we will ignore this error, but in # the longer term we need to implement some form of locking, perhaps. if calendar is None: log.error( "Calendar %s is missing from calendar collection %r" % (name, calresource)) continue # Ignore ones of this UID if excludeuid: # See if we have a UID match if (excludeuid == uid): test_organizer = calendar.getOrganizer() test_principal = ( yield calresource.principalForCalendarUserAddress( test_organizer)) if test_organizer else None test_uid = test_principal.principalUID( ) if test_principal else "" # Check that ORGANIZER's match (security requirement) if (organizer is None) or (organizer_uid == test_uid): continue # Check for no ORGANIZER and check by same calendar user elif (test_organizer is None) and same_calendar_user: continue if filter.match(calendar, None): # Check size of results is within limit matchtotal += 1 if matchtotal > max_number_of_matches: raise NumberOfMatchesWithinLimits(max_number_of_matches) if calendar.mainType() == "VEVENT": processEventFreeBusy(calendar, fbinfo, timerange, tzinfo) elif calendar.mainType() == "VFREEBUSY": processFreeBusyFreeBusy(calendar, fbinfo, timerange) elif calendar.mainType() == "VAVAILABILITY": processAvailabilityFreeBusy(calendar, fbinfo, timerange) else: assert "Free-busy query returned unwanted component: %s in %r", ( name, calresource, ) # Add extended details if calendar.mainType() == "VEVENT" and do_event_details: child = (yield request.locateChildResource(calresource, name)) calendar = (yield child.iCalendarForUser(request)) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) returnValue(matchtotal)
def _internalGenerateFreeBusyInfo( calresource, fbinfo, timerange, matchtotal, excludeuid=None, organizer=None, organizerPrincipal=None, same_calendar_user=False, servertoserver=False, event_details=None, logItems=None, accountingItems=None, ): """ Run a free busy report on the specified calendar collection accumulating the free busy info for later processing. @param calresource: the L{Calendar} for a calendar collection. @param fbinfo: the array of busy periods to update. @param timerange: the L{TimeRange} for the query. @param matchtotal: the running total for the number of matches. @param excludeuid: a C{str} containing a UID value to exclude any components with that UID from contributing to free-busy. @param organizer: a C{str} containing the value of the ORGANIZER property in the VFREEBUSY request. This is used in conjunction with the UID value to process exclusions. @param same_calendar_user: a C{bool} indicating whether the calendar user requesting the free-busy information is the same as the calendar user being targeted. @param servertoserver: a C{bool} indicating whether we are doing a local or remote lookup request. @param event_details: a C{list} into which to store extended VEVENT details if not C{None} @param logItems: a C{dict} to store logging info to @param accountingItems: a C{dict} to store accounting info to """ # First check the privilege on this collection # TODO: for server-to-server we bypass this right now as we have no way to authorize external users. # TODO: actually we by pass altogether by assuming anyone can check anyone else's freebusy # May need organizer principal organizer_record = (yield calresource.directoryService().recordWithCalendarUserAddress(organizer)) if organizer else None organizer_uid = organizer_record.uid if organizer_record else "" # Free busy is per-user attendee_uid = calresource.viewerHome().uid() attendee_record = yield calresource.directoryService().recordWithUID(attendee_uid.decode("utf-8")) # Get the timezone property from the collection. tz = calresource.getTimezone() # Look for possible extended free busy information rich_options = { "organizer": False, "delegate": False, "resource": False, } do_event_details = False if event_details is not None and organizer_record is not None and attendee_record is not None: # Get the principal of the authorized user which may be different from the organizer if a delegate of # the organizer is making the request authz_uid = organizer_uid authz_record = organizer_record if calresource._txn._authz_uid is not None and calresource._txn._authz_uid != organizer_uid: authz_uid = calresource._txn._authz_uid authz_record = yield calresource.directoryService().recordWithUID(authz_uid.decode("utf-8")) # Check if attendee is also the organizer or the delegate doing the request if attendee_uid in (organizer_uid, authz_uid): do_event_details = True rich_options["organizer"] = True # Check if authorized user is a delegate of attendee proxy = (yield authz_record.isProxyFor(attendee_record)) if config.Scheduling.Options.DelegeteRichFreeBusy and proxy: do_event_details = True rich_options["delegate"] = True # Check if attendee is room or resource if config.Scheduling.Options.RoomResourceRichFreeBusy and attendee_record.getCUType() in ("RESOURCE", "ROOM",): do_event_details = True rich_options["resource"] = True # Try cache resources = (yield FBCacheEntry.getCacheEntry(calresource, attendee_uid, timerange)) if config.EnableFreeBusyCache else None if resources is None: if accountingItems is not None: accountingItems["fb-uncached"] = accountingItems.get("fb-uncached", 0) + 1 caching = False if config.EnableFreeBusyCache: # Log extended item if logItems is not None: logItems["fb-uncached"] = logItems.get("fb-uncached", 0) + 1 # We want to cache a large range of time based on the current date cache_start = normalizeToUTC(DateTime.getToday() + Duration(days=0 - config.FreeBusyCacheDaysBack)) cache_end = normalizeToUTC(DateTime.getToday() + Duration(days=config.FreeBusyCacheDaysForward)) # If the requested time range would fit in our allowed cache range, trigger the cache creation if compareDateTime(timerange.start, cache_start) >= 0 and compareDateTime(timerange.end, cache_end) <= 0: cache_timerange = TimeRange(start=cache_start.getText(), end=cache_end.getText()) caching = True # # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range. # We then take those results and merge them into one VFREEBUSY component # with appropriate FREEBUSY properties, and return that single item as iCal data. # # Create fake filter element to match time-range filter = caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( cache_timerange if caching else timerange, name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"), ), name="VCALENDAR", ) ) filter = Filter(filter) tzinfo = filter.settimezone(tz) if accountingItems is not None: tr = cache_timerange if caching else timerange accountingItems["fb-query-timerange"] = (str(tr.start), str(tr.end),) try: resources = yield calresource.search(filter, useruid=attendee_uid, fbtype=True) if caching: yield FBCacheEntry.makeCacheEntry(calresource, attendee_uid, cache_timerange, resources) except IndexedSearchException: raise InternalDataStoreError("Invalid indexedSearch query") else: if accountingItems is not None: accountingItems["fb-cached"] = accountingItems.get("fb-cached", 0) + 1 # Log extended item if logItems is not None: logItems["fb-cached"] = logItems.get("fb-cached", 0) + 1 # Determine appropriate timezone (UTC is the default) tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True) # We care about separate instances for VEVENTs only aggregated_resources = {} for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources: if transp == 'T' and fbtype != '?': fbtype = 'F' aggregated_resources.setdefault((name, uid, type, test_organizer,), []).append((float, start, end, fbtype,)) if accountingItems is not None: accountingItems["fb-resources"] = {} for k, v in aggregated_resources.items(): name, uid, type, test_organizer = k accountingItems["fb-resources"][uid] = [] for float, start, end, fbtype in v: fbstart = parseSQLTimestampToPyCalendar(start) if float == 'Y': fbstart.setTimezone(tzinfo) else: fbstart.setTimezone(Timezone(utc=True)) fbend = parseSQLTimestampToPyCalendar(end) if float == 'Y': fbend.setTimezone(tzinfo) else: fbend.setTimezone(Timezone(utc=True)) accountingItems["fb-resources"][uid].append(( float, str(fbstart), str(fbend), fbtype, )) for key in aggregated_resources.iterkeys(): name, uid, type, test_organizer = key # Short-cut - if an fbtype exists we can use that if type == "VEVENT" and aggregated_resources[key][0][3] != '?': matchedResource = False # Look at each instance for float, start, end, fbtype in aggregated_resources[key]: # Ignore free time or unknown if fbtype in ('F', '?'): continue # Ignore ones of this UID if excludeuid: # See if we have a UID match if (excludeuid == uid): test_record = (yield calresource.directoryService().recordWithCalendarUserAddress(test_organizer)) if test_organizer else None test_uid = test_record.uid if test_record else "" # Check that ORGANIZER's match (security requirement) if (organizer is None) or (organizer_uid == test_uid): continue # Check for no ORGANIZER and check by same calendar user elif (test_uid == "") and same_calendar_user: continue # Apply a timezone to any floating times fbstart = parseSQLTimestampToPyCalendar(start) if float == 'Y': fbstart.setTimezone(tzinfo) else: fbstart.setTimezone(Timezone(utc=True)) fbend = parseSQLTimestampToPyCalendar(end) if float == 'Y': fbend.setTimezone(tzinfo) else: fbend.setTimezone(Timezone(utc=True)) # Clip instance to time range clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), Period(timerange.start, timerange.end)) # Double check for overlap if clipped: matchedResource = True fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped) if matchedResource: # Check size of results is within limit matchtotal += 1 if matchtotal > config.MaxQueryWithDataResults: raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal) # Add extended details if do_event_details: child = (yield calresource.calendarObjectWithName(name)) # Only add fully public events if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC: calendar = (yield child.componentForUser()) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) else: child = (yield calresource.calendarObjectWithName(name)) calendar = (yield child.componentForUser()) # The calendar may come back as None if the resource is being changed, or was deleted # between our initial index query and getting here. For now we will ignore this error, but in # the longer term we need to implement some form of locking, perhaps. if calendar is None: log.error("Calendar %s is missing from calendar collection %r" % (name, calresource)) continue # Ignore ones of this UID if excludeuid: # See if we have a UID match if (excludeuid == uid): test_organizer = calendar.getOrganizer() test_record = (yield calresource.principalForCalendarUserAddress(test_organizer)) if test_organizer else None test_uid = test_record.principalUID() if test_record else "" # Check that ORGANIZER's match (security requirement) if (organizer is None) or (organizer_uid == test_uid): continue # Check for no ORGANIZER and check by same calendar user elif (test_organizer is None) and same_calendar_user: continue if accountingItems is not None: accountingItems.setdefault("fb-filter-match", []).append(uid) if filter.match(calendar, None): if accountingItems is not None: accountingItems.setdefault("fb-filter-matched", []).append(uid) # Check size of results is within limit matchtotal += 1 if matchtotal > config.MaxQueryWithDataResults: raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal) if calendar.mainType() == "VEVENT": processEventFreeBusy(calendar, fbinfo, timerange, tzinfo) elif calendar.mainType() == "VFREEBUSY": processFreeBusyFreeBusy(calendar, fbinfo, timerange) elif calendar.mainType() == "VAVAILABILITY": processAvailabilityFreeBusy(calendar, fbinfo, timerange) else: assert "Free-busy query returned unwanted component: %s in %r", (name, calresource,) # Add extended details if calendar.mainType() == "VEVENT" and do_event_details: child = (yield calresource.calendarObjectWithName(name)) # Only add fully public events if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC: calendar = (yield child.componentForUser()) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) returnValue(matchtotal)