Пример #1
0
    def processFreeBusyFreeBusy(self, calendar, fbinfo):
        """
        Extract FREEBUSY data from a VFREEBUSY component.
        @param calendar: the L{Component} that is the VCALENDAR containing the VFREEBUSY's.
        @param fbinfo: the tuple used to store the three types of fb data.
        """

        for vfb in [x for x in calendar.subcomponents() if x.name() == "VFREEBUSY"]:
            # First check any start/end in the actual component
            start = vfb.getStartDateUTC()
            end = vfb.getEndDateUTC()
            if start and end:
                if not timeRangesOverlap(start, end, self.timerange.getStart(), self.timerange.getEnd()):
                    continue

            # Now look at each FREEBUSY property
            for fb in vfb.properties("FREEBUSY"):
                # Check the type
                fbtype = fb.parameterValue("FBTYPE", default="BUSY")
                if fbtype == "FREE":
                    continue

                # Look at each period in the property
                assert isinstance(fb.value(), list), "FREEBUSY property does not contain a list of values: %r" % (fb,)
                for period in fb.value():
                    # Clip period for this instance
                    clipped = clipPeriod(period.getValue(), self.timerange)
                    if clipped:
                        getattr(fbinfo, self.FBInfo_mapper.get(fbtype, "busy")).append(clipped)
Пример #2
0
    def limitFreeBusy(self, calendar):
        """
        Limit the range of any FREEBUSY properties in the calendar, returning
        a new calendar if limits were applied, or the same one if no limits were applied.
        @param calendar: the L{Component} for the calendar to operate on.
        @return: the L{Component} for the result.
        """

        # First check for any VFREEBUSYs - can ignore limit if there are none
        if calendar.mainType() != "VFREEBUSY":
            return calendar

        # Create duplicate calendar and filter FREEBUSY properties
        calendar = calendar.duplicate()
        for component in calendar.subcomponents():
            if component.name() != "VFREEBUSY":
                continue
            for property in component.properties("FREEBUSY"):
                newvalue = []
                for period in property.value():
                    clipped = clipPeriod(period.getValue(), Period(self.calendardata.freebusy_set.start, self.calendardata.freebusy_set.end))
                    if clipped:
                        newvalue.append(clipped)
                if len(newvalue):
                    property.setValue(newvalue)
                else:
                    component.removeProperty(property)
        return calendar
Пример #3
0
def processFreeBusyFreeBusy(calendar, fbinfo, timerange):
    """
    Extract FREEBUSY data from a VFREEBUSY component.
    @param calendar: the L{Component} that is the VCALENDAR containing the VFREEBUSY's.
    @param fbinfo: the tuple used to store the three types of fb data.
    @param timerange: the time range to restrict free busy data to.
    """

    for vfb in [x for x in calendar.subcomponents() if x.name() == "VFREEBUSY"]:
        # First check any start/end in the actual component
        start = vfb.getStartDateUTC()
        end = vfb.getEndDateUTC()
        if start and end:
            if not timeRangesOverlap(start, end, timerange.start, timerange.end):
                continue

        # Now look at each FREEBUSY property
        for fb in vfb.properties("FREEBUSY"):
            # Check the type
            fbtype = fb.parameterValue("FBTYPE", default="BUSY")
            if fbtype == "FREE":
                continue

            # Look at each period in the property
            assert isinstance(fb.value(), list), "FREEBUSY property does not contain a list of values: %r" % (fb,)
            for period in fb.value():
                # Clip period for this instance
                clipped = clipPeriod(period.getValue(), PyCalendarPeriod(timerange.start, timerange.end))
                if clipped:
                    fbinfo[fbtype_mapper.get(fbtype, 0)].append(clipped)
Пример #4
0
    def limitFreeBusy(self, calendar):
        """
        Limit the range of any FREEBUSY properties in the calendar, returning
        a new calendar if limits were applied, or the same one if no limits were applied.
        @param calendar: the L{Component} for the calendar to operate on.
        @return: the L{Component} for the result.
        """

        # First check for any VFREEBUSYs - can ignore limit if there are none
        if calendar.mainType() != "VFREEBUSY":
            return calendar

        # Create duplicate calendar and filter FREEBUSY properties
        calendar = calendar.duplicate()
        for component in calendar.subcomponents():
            if component.name() != "VFREEBUSY":
                continue
            for property in component.properties("FREEBUSY"):
                newvalue = []
                for period in property.value():
                    clipped = clipPeriod(period.getValue(), PyCalendarPeriod(self.calendardata.freebusy_set.start, self.calendardata.freebusy_set.end))
                    if clipped:
                        newvalue.append(clipped)
                if len(newvalue):
                    property.setValue(newvalue)
                else:
                    component.removeProperty(property)
        return calendar
Пример #5
0
    def processAvailabilityFreeBusy(self, calendar, fbinfo):
        """
        Extract free-busy data from a VAVAILABILITY component.

        @param calendar: the L{Component} that is the VCALENDAR containing the VAVAILABILITY's.
        @param fbinfo: the tuple used to store the three types of fb data.
        """

        for vav in [
                x for x in calendar.subcomponents()
                if x.name() == "VAVAILABILITY"
        ]:

            # Get overall start/end
            start = vav.getStartDateUTC()
            if start is None:
                start = DateTime(1900,
                                 1,
                                 1,
                                 0,
                                 0,
                                 0,
                                 tzid=Timezone.UTCTimezone)
            end = vav.getEndDateUTC()
            if end is None:
                end = DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone.UTCTimezone)
            period = Period(start, end)
            overall = clipPeriod(period, self.timerange)
            if overall is None:
                continue

            # Now get periods for each instance of AVAILABLE sub-components
            periods = self.processAvailablePeriods(vav)

            # Now invert the periods and store in accumulator
            busyperiods = []
            last_end = self.timerange.getStart()
            for period in periods:
                if last_end < period.getStart():
                    busyperiods.append(Period(last_end, period.getStart()))
                last_end = period.getEnd()
            if last_end < self.timerange.getEnd():
                busyperiods.append(Period(last_end, self.timerange.getEnd()))

            # Add to actual results mapped by busy type
            fbtype = vav.propertyValue("BUSYTYPE")
            if fbtype is None:
                fbtype = "BUSY-UNAVAILABLE"

            getattr(fbinfo,
                    self.FBInfo_mapper.get(fbtype,
                                           "unavailable")).extend(busyperiods)
Пример #6
0
def processAvailablePeriods(calendar, timerange):
    """
    Extract instance period data from an AVAILABLE component.
    @param calendar: the L{Component} that is the VAVAILABILITY containing the AVAILABLE's.
    @param timerange: the time range to restrict free busy data to.
    """

    periods = []

    # First we need to group all AVAILABLE sub-components by UID
    uidmap = {}
    for component in calendar.subcomponents():
        if component.name() == "AVAILABLE":
            uid = component.propertyValue("UID")
            uidmap.setdefault(uid, []).append(component)

    # Then we expand each uid set separately
    for componentSet in uidmap.itervalues():
        instances = InstanceList(ignoreInvalidInstances=True)
        instances.expandTimeRanges(componentSet, timerange.end)

        # Now convert instances into period list
        for key in instances:
            instance = instances[key]
            # Ignore any with floating times (which should not happen as the spec requires UTC or local
            # but we will try and be safe here).
            start = instance.start
            if start.floating():
                continue
            end = instance.end
            if end.floating():
                continue

            # Clip period for this instance - use duration for period end if that
            # is what original component used
            if instance.component.hasProperty("DURATION"):
                period = Period(start, duration=end - start)
            else:
                period = Period(start, end)
            clipped = clipPeriod(period, Period(timerange.start,
                                                timerange.end))
            if clipped:
                periods.append(clipped)

    normalizePeriodList(periods)
    return periods
Пример #7
0
def processAvailablePeriods(calendar, timerange):
    """
    Extract instance period data from an AVAILABLE component.
    @param calendar: the L{Component} that is the VAVAILABILITY containing the AVAILABLE's.
    @param timerange: the time range to restrict free busy data to.
    """

    periods = []

    # First we need to group all AVAILABLE sub-components by UID
    uidmap = {}
    for component in calendar.subcomponents():
        if component.name() == "AVAILABLE":
            uid = component.propertyValue("UID")
            uidmap.setdefault(uid, []).append(component)

    # Then we expand each uid set separately
    for componentSet in uidmap.itervalues():
        instances = InstanceList(ignoreInvalidInstances=True)
        instances.expandTimeRanges(componentSet, timerange.end)

        # Now convert instances into period list
        for key in instances:
            instance = instances[key]
            # Ignore any with floating times (which should not happen as the spec requires UTC or local
            # but we will try and be safe here).
            start = instance.start
            if start.floating():
                continue
            end = instance.end
            if end.floating():
                continue

            # Clip period for this instance - use duration for period end if that
            # is what original component used
            if instance.component.hasProperty("DURATION"):
                period = PyCalendarPeriod(start, duration=end - start)
            else:
                period = PyCalendarPeriod(start, end)
            clipped = clipPeriod(period, PyCalendarPeriod(timerange.start, timerange.end))
            if clipped:
                periods.append(clipped)

    normalizePeriodList(periods)
    return periods
Пример #8
0
def processAvailabilityFreeBusy(calendar, fbinfo, timerange):
    """
    Extract free-busy data from a VAVAILABILITY component.
    @param calendar: the L{Component} that is the VCALENDAR containing the VAVAILABILITY's.
    @param fbinfo: the tuple used to store the three types of fb data.
    @param timerange: the time range to restrict free busy data to.
    """

    for vav in [x for x in calendar.subcomponents() if x.name() == "VAVAILABILITY"]:

        # Get overall start/end
        start = vav.getStartDateUTC()
        if start is None:
            start = PyCalendarDateTime(1900, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
        end = vav.getEndDateUTC()
        if end is None:
            end = PyCalendarDateTime(2100, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
        period = PyCalendarPeriod(start, end)
        overall = clipPeriod(period, PyCalendarPeriod(timerange.start, timerange.end))
        if overall is None:
            continue

        # Now get periods for each instance of AVAILABLE sub-components
        periods = processAvailablePeriods(vav, timerange)

        # Now invert the periods and store in accumulator
        busyperiods = []
        last_end = timerange.start
        for period in periods:
            if last_end < period.getStart():
                busyperiods.append(PyCalendarPeriod(last_end, period.getStart()))
            last_end = period.getEnd()
        if last_end < timerange.end:
            busyperiods.append(PyCalendarPeriod(last_end, timerange.end))

        # Add to actual results mapped by busy type
        fbtype = vav.propertyValue("BUSYTYPE")
        if fbtype is None:
            fbtype = "BUSY-UNAVAILABLE"

        fbinfo[fbtype_mapper.get(fbtype, 2)].extend(busyperiods)
Пример #9
0
    def processAvailabilityFreeBusy(self, calendar, fbinfo):
        """
        Extract free-busy data from a VAVAILABILITY component.

        @param calendar: the L{Component} that is the VCALENDAR containing the VAVAILABILITY's.
        @param fbinfo: the tuple used to store the three types of fb data.
        """

        for vav in [x for x in calendar.subcomponents() if x.name() == "VAVAILABILITY"]:

            # Get overall start/end
            start = vav.getStartDateUTC()
            if start is None:
                start = DateTime(1900, 1, 1, 0, 0, 0, tzid=Timezone.UTCTimezone)
            end = vav.getEndDateUTC()
            if end is None:
                end = DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone.UTCTimezone)
            period = Period(start, end)
            overall = clipPeriod(period, self.timerange)
            if overall is None:
                continue

            # Now get periods for each instance of AVAILABLE sub-components
            periods = self.processAvailablePeriods(vav)

            # Now invert the periods and store in accumulator
            busyperiods = []
            last_end = self.timerange.getStart()
            for period in periods:
                if last_end < period.getStart():
                    busyperiods.append(Period(last_end, period.getStart()))
                last_end = period.getEnd()
            if last_end < self.timerange.getEnd():
                busyperiods.append(Period(last_end, self.timerange.getEnd()))

            # Add to actual results mapped by busy type
            fbtype = vav.propertyValue("BUSYTYPE")
            if fbtype is None:
                fbtype = "BUSY-UNAVAILABLE"

            getattr(fbinfo, self.FBInfo_mapper.get(fbtype, "unavailable")).extend(busyperiods)
Пример #10
0
def generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchtotal,
                         excludeuid=None, organizer=None, organizerPrincipal=None, same_calendar_user=False,
                         servertoserver=False):
    """
    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.
    """
    
    # 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 = calresource.principalForCalendarUserAddress(organizer) if organizer else None
    organizer_uid = organizer_principal.principalUID() if organizer_principal else ""

    #
    # 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(
                          timerange,
                          name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
                      ),
                      name="VCALENDAR",
                   )
              )
    filter = calendarqueryfilter.Filter(filter)

    # Get the timezone property from the collection, and store in the query filter
    # for use during the query itself.
    has_prop = (yield calresource.hasProperty((caldav_namespace, "calendar-timezone"), request))
    if has_prop:
        tz = (yield calresource.readProperty((caldav_namespace, "calendar-timezone"), request))
    else:
        tz = None
    tzinfo = filter.settimezone(tz)

    # 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))

    try:
        useruid = (yield calresource.resourceOwnerPrincipal(request))
        useruid = useruid.principalUID() if useruid else ""
        resources = calresource.index().indexedSearch(filter, useruid=useruid, fbtype=True)
    except IndexedSearchException:
        resources = calresource.index().bruteForceSearch()

    # 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

        # Check privileges - must have at least CalDAV:read-free-busy
        child = (yield request.locateChildResource(calresource, name))

        # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
        if not servertoserver:
            try:
                yield child.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), inherited_aces=filteredaces, principal=organizerPrincipal)
            except AccessDeniedError:
                continue

        # 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 = 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 = datetime.datetime.strptime(start[:19], "%Y-%m-%d %H:%M:%S")
                if float == 'Y':
                    fbstart = fbstart.replace(tzinfo=tzinfo)
                else:
                    fbstart = fbstart.replace(tzinfo=utc)
                fbend =datetime.datetime.strptime(end[:19], "%Y-%m-%d %H:%M:%S")
                if float == 'Y':
                    fbend = fbend.replace(tzinfo=tzinfo)
                else:
                    fbend = fbend.replace(tzinfo=utc)
                
                # Click instance to time range
                clipped = clipPeriod((fbstart, fbend - fbstart), (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)
                
        else:
            calendar = (yield calresource.iCalendarForUser(request, name))
            
            # 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.err("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 = 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,)
    
    returnValue(matchtotal)
Пример #11
0
    def processEventFreeBusy(self, calendar, fbinfo, tzinfo):
        """
        Extract free busy data from a VEVENT component.
        @param calendar: the L{Component} that is the VCALENDAR containing the VEVENT's.
        @param fbinfo: the tuple used to store the three types of fb data.
        @param tzinfo: the L{Timezone} for the timezone to use for floating/all-day events.
        """

        # Expand out the set of instances for the event with in the required range
        instances = calendar.expandTimeRanges(self.timerange.getEnd(), lowerLimit=self.timerange.getStart(), ignoreInvalidInstances=True)

        # Can only do timed events
        for key in instances:
            instance = instances[key]
            if instance.start.isDateOnly():
                return
            break
        else:
            return

        for key in instances:
            instance = instances[key]

            # Apply a timezone to any floating times
            fbstart = instance.start
            if fbstart.floating():
                fbstart.setTimezone(tzinfo)
            fbend = instance.end
            if fbend.floating():
                fbend.setTimezone(tzinfo)

            # Check TRANSP property of underlying component
            if instance.component.hasProperty("TRANSP"):
                # If its TRANSPARENT we always ignore it
                if instance.component.propertyValue("TRANSP") == "TRANSPARENT":
                    continue

            # Determine status
            if instance.component.hasProperty("STATUS"):
                status = instance.component.propertyValue("STATUS")
            else:
                status = "CONFIRMED"

            # Ignore cancelled
            if status == "CANCELLED":
                continue

            # Clip period for this instance - use duration for period end if that
            # is what original component used
            if instance.component.hasProperty("DURATION"):
                period = Period(fbstart, duration=fbend - fbstart)
            else:
                period = Period(fbstart, fbend)
            clipped = clipPeriod(period, self.timerange)

            # Double check for overlap
            if clipped:
                if status == "TENTATIVE":
                    fbinfo.tentative.append(clipped)
                else:
                    fbinfo.busy.append(clipped)
Пример #12
0
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)
Пример #13
0
    def _internalGenerateFreeBusyInfo(
        self,
        fbset,
        fbinfo,
        matchtotal,
    ):
        """
        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 matchtotal:  the running total for the number of matches.
        """

        yield self.checkRichOptions(fbset[0]._txn)

        calidmap = dict([(fbcalendar.id(), fbcalendar,) for fbcalendar in fbset])
        directoryService = fbset[0].directoryService()

        results = yield self._matchResources(fbset)

        if self.accountingItems is not None:
            self.accountingItems["fb-resources"] = {}
            for calid, result in results.items():
                aggregated_resources, tzinfo, filter = result
                for k, v in aggregated_resources.items():
                    name, uid, comptype, test_organizer = k
                    self.accountingItems["fb-resources"][uid] = []
                    for float, start, end, fbtype in v:
                        fbstart = tupleToDateTime(start, withTimezone=tzinfo if float == 'Y' else Timezone.UTCTimezone)
                        fbend = tupleToDateTime(end, withTimezone=tzinfo if float == 'Y' else Timezone.UTCTimezone)
                        self.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 calid, result in results.items():
            calresource = calidmap[calid]
            aggregated_resources, tzinfo, filter = result
            for key in aggregated_resources.iterkeys():

                name, uid, comptype, test_organizer = key

                # Short-cut - if an fbtype exists we can use that
                if comptype == "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

                        # Apply a timezone to any floating times
                        fbstart = tupleToDateTime(start, withTimezone=tzinfo if float == 'Y' else Timezone.UTCTimezone)
                        fbend = tupleToDateTime(end, withTimezone=tzinfo if float == 'Y' else Timezone.UTCTimezone)

                        # Clip instance to time range
                        clipped = clipPeriod(Period(fbstart, end=fbend), self.timerange)

                        # Double check for overlap
                        if clipped:
                            # Ignore ones of this UID
                            if not (yield self._testIgnoreExcludeUID(uid, test_organizer, recordUIDCache, directoryService)):
                                clipped.setUseDuration(True)
                                matchedResource = True
                                getattr(fbinfo, self.FBInfo_index_mapper.get(fbtype, "busy")).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 any(self.rich_options.values()):
                            child = (yield calresource.calendarObjectWithName(name))
                            # Only add fully public events
                            if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
                                calendar = (yield child.componentForUser())
                                self._addEventDetails(calendar, self.rich_options, 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 {name} is missing from calendar collection {coll!r}", name=name, coll=calresource)
                        continue

                    if self.accountingItems is not None:
                        self.accountingItems.setdefault("fb-filter-match", []).append(uid)

                    if filter.match(calendar, None):

                        # Ignore ones of this UID
                        if (yield self._testIgnoreExcludeUID(uid, calendar.getOrganizer(), recordUIDCache, calresource.directoryService())):
                            continue

                        if self.accountingItems is not None:
                            self.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":
                            self.processEventFreeBusy(calendar, fbinfo, tzinfo)
                        elif calendar.mainType() == "VFREEBUSY":
                            self.processFreeBusyFreeBusy(calendar, fbinfo)
                        elif calendar.mainType() == "VAVAILABILITY":
                            self.processAvailabilityFreeBusy(calendar, fbinfo)
                        else:
                            assert "Free-busy query returned unwanted component: %s in %r", (name, calresource,)

                        # Add extended details
                        if calendar.mainType() == "VEVENT" and any(self.rich_options.values()):
                            # Only add fully public events
                            if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
                                self._addEventDetails(calendar, self.rich_options, tzinfo)

        returnValue(matchtotal)
Пример #14
0
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)
Пример #15
0
def generateFreeBusyInfo(
    calresource,
    fbinfo,
    timerange,
    matchtotal,
    excludeuid=None,
    organizer=None,
    organizerPrincipal=None,
    same_calendar_user=False,
    servertoserver=False,
    event_details=None,
    logItems=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
    """

    # 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 = 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 = calresource.directoryService().recordWithUID(attendee_uid)

    # 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 hasattr(calresource._txn, "_authz_uid") and calresource._txn._authz_uid != organizer_uid:
            authz_uid = calresource._txn._authz_uid
            authz_record = calresource.directoryService().recordWithUID(authz_uid)

        # 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:

        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(PyCalendarDateTime.getToday() + PyCalendarDuration(days=0 - config.FreeBusyCacheDaysBack))
            cache_end = normalizeToUTC(PyCalendarDateTime.getToday() + PyCalendarDuration(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 = calendarqueryfilter.Filter(filter)
        tzinfo = filter.settimezone(tz)

        try:
            resources = yield calresource._index.indexedSearch(filter, useruid=attendee_uid, fbtype=True)
            if caching:
                yield FBCacheEntry.makeCacheEntry(calresource, attendee_uid, cache_timerange, resources)
        except IndexedSearchException:
            resources = yield calresource._index.bruteForceSearch()

    else:
        # 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 PyCalendarTimezone(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_record = 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(PyCalendarTimezone(utc=True))
                fbend = parseSQLTimestampToPyCalendar(end)
                if float == 'Y':
                    fbend.setTimezone(tzinfo)
                else:
                    fbend.setTimezone(PyCalendarTimezone(utc=True))

                # Clip instance to time range
                clipped = clipPeriod(PyCalendarPeriod(fbstart, duration=fbend - fbstart), PyCalendarPeriod(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 = 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 filter.match(calendar, None):
                # 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)
Пример #16
0
def processEventFreeBusy(calendar, fbinfo, timerange, tzinfo):
    """
    Extract free busy data from a VEVENT component.
    @param calendar: the L{Component} that is the VCALENDAR containing the VEVENT's.
    @param fbinfo: the tuple used to store the three types of fb data.
    @param timerange: the time range to restrict free busy data to.
    @param tzinfo: the L{PyCalendarTimezone} for the timezone to use for floating/all-day events.
    """

    # Expand out the set of instances for the event with in the required range
    instances = calendar.expandTimeRanges(timerange.end, lowerLimit=timerange.start, ignoreInvalidInstances=True)

    # Can only do timed events
    for key in instances:
        instance = instances[key]
        if instance.start.isDateOnly():
            return
        break
    else:
        return

    for key in instances:
        instance = instances[key]

        # Apply a timezone to any floating times
        fbstart = instance.start
        if fbstart.floating():
            fbstart.setTimezone(tzinfo)
        fbend = instance.end
        if fbend.floating():
            fbend.setTimezone(tzinfo)

        # Check TRANSP property of underlying component
        if instance.component.hasProperty("TRANSP"):
            # If its TRANSPARENT we always ignore it
            if instance.component.propertyValue("TRANSP") == "TRANSPARENT":
                continue

        # Determine status
        if instance.component.hasProperty("STATUS"):
            status = instance.component.propertyValue("STATUS")
        else:
            status = "CONFIRMED"

        # Ignore cancelled
        if status == "CANCELLED":
            continue

        # Clip period for this instance - use duration for period end if that
        # is what original component used
        if instance.component.hasProperty("DURATION"):
            period = PyCalendarPeriod(fbstart, duration=fbend - fbstart)
        else:
            period = PyCalendarPeriod(fbstart, fbend)
        clipped = clipPeriod(period, PyCalendarPeriod(timerange.start, timerange.end))

        # Double check for overlap
        if clipped:
            if status == "TENTATIVE":
                fbinfo[1].append(clipped)
            else:
                fbinfo[0].append(clipped)
Пример #17
0
    def _internalGenerateFreeBusyInfo(
        self,
        fbset,
        fbinfo,
        matchtotal,
    ):
        """
        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 matchtotal:  the running total for the number of matches.
        """

        yield self.checkRichOptions(fbset[0]._txn)

        calidmap = dict([(fbcalendar.id(), fbcalendar,) for fbcalendar in fbset])
        directoryService = fbset[0].directoryService()

        results = yield self._matchResources(fbset)

        if self.accountingItems is not None:
            self.accountingItems["fb-resources"] = {}
            for calid, result in results.items():
                aggregated_resources, tzinfo, filter = result
                for k, v in aggregated_resources.items():
                    name, uid, comptype, test_organizer = k
                    self.accountingItems["fb-resources"][uid] = []
                    for float, start, end, fbtype in v:
                        fbstart = tupleToDateTime(start, withTimezone=tzinfo if float == 'Y' else Timezone.UTCTimezone)
                        fbend = tupleToDateTime(end, withTimezone=tzinfo if float == 'Y' else Timezone.UTCTimezone)
                        self.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 calid, result in results.items():
            calresource = calidmap[calid]
            aggregated_resources, tzinfo, filter = result
            for key in aggregated_resources.iterkeys():

                name, uid, comptype, test_organizer = key

                # Short-cut - if an fbtype exists we can use that
                if comptype == "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

                        # Apply a timezone to any floating times
                        fbstart = tupleToDateTime(start, withTimezone=tzinfo if float == 'Y' else Timezone.UTCTimezone)
                        fbend = tupleToDateTime(end, withTimezone=tzinfo if float == 'Y' else Timezone.UTCTimezone)

                        # Clip instance to time range
                        clipped = clipPeriod(Period(fbstart, end=fbend), self.timerange)

                        # Double check for overlap
                        if clipped:
                            # Ignore ones of this UID
                            if not (yield self._testIgnoreExcludeUID(uid, test_organizer, recordUIDCache, directoryService)):
                                clipped.setUseDuration(True)
                                matchedResource = True
                                getattr(fbinfo, self.FBInfo_index_mapper.get(fbtype, "busy")).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 any(self.rich_options.values()):
                            child = (yield calresource.calendarObjectWithName(name))
                            # Only add fully public events
                            if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
                                calendar = (yield child.componentForUser())
                                self._addEventDetails(calendar, self.rich_options, 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

                    if self.accountingItems is not None:
                        self.accountingItems.setdefault("fb-filter-match", []).append(uid)

                    if filter.match(calendar, None):

                        # Ignore ones of this UID
                        if (yield self._testIgnoreExcludeUID(uid, calendar.getOrganizer(), recordUIDCache, calresource.directoryService())):
                            continue

                        if self.accountingItems is not None:
                            self.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":
                            self.processEventFreeBusy(calendar, fbinfo, tzinfo)
                        elif calendar.mainType() == "VFREEBUSY":
                            self.processFreeBusyFreeBusy(calendar, fbinfo)
                        elif calendar.mainType() == "VAVAILABILITY":
                            self.processAvailabilityFreeBusy(calendar, fbinfo)
                        else:
                            assert "Free-busy query returned unwanted component: %s in %r", (name, calresource,)

                        # Add extended details
                        if calendar.mainType() == "VEVENT" and any(self.rich_options.values()):
                            # Only add fully public events
                            if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
                                self._addEventDetails(calendar, self.rich_options, tzinfo)

        returnValue(matchtotal)