def report_urn_ietf_params_xml_ns_caldav_free_busy_query(self, request, freebusy): # @UnusedVariable """ Generate a free-busy REPORT. (CalDAV-access-09, section 7.8) """ if not self.isCollection(): log.error("freebusy report is only allowed on collection resources %s" % (self,)) raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Not a calendar collection")) if freebusy.qname() != (caldavxml.caldav_namespace, "free-busy-query"): raise ValueError("{CalDAV:}free-busy-query expected as root element, not %s." % (freebusy.sname(),)) timerange = freebusy.timerange if not timerange.valid(): raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid time-range specified")) # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE fbinfo = ([], [], []) matchcount = [0] accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes()) if accepted_type is None: raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type")) def generateFreeBusyInfo(calresource, uri): # @UnusedVariable """ Run a free busy report on the specified calendar collection accumulating the free busy info for later processing. @param calresource: the L{CalDAVResource} for a calendar collection. @param uri: the uri for the calendar collecton resource. """ def _gotResult(result): matchcount[0] = result return True d = report_common.generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchcount[0]) d.addCallback(_gotResult) return d # Run report taking depth into account try: depth = request.headers.getHeader("depth", "0") yield report_common.applyToCalendarCollections( self, request, request.uri, depth, generateFreeBusyInfo, (caldavxml.ReadFreeBusy(),) ) except NumberOfMatchesWithinLimits: log.error("Too many matching components in free-busy report") raise HTTPError( ErrorResponse(responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits(), "Too many components") ) except TimeRangeLowerLimit, e: raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, caldavxml.MinDateTime(), "Time-range value too far in the past. Must be on or after %s." % (str(e.limit),), ) )
def report_urn_ietf_params_xml_ns_caldav_free_busy_query(self, request, freebusy): """ Generate a free-busy REPORT. (CalDAV-access-09, section 7.8) """ if not self.isCollection(): log.error("freebusy report is only allowed on collection resources %s" % (self,)) raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Not a calendar collection")) if freebusy.qname() != (caldavxml.caldav_namespace, "free-busy-query"): raise ValueError("{CalDAV:}free-busy-query expected as root element, not %s." % (freebusy.sname(),)) timerange = freebusy.timerange if not timerange.valid(): raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid time-range specified")) fbset = [] accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes()) if accepted_type is None: raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type")) def getCalendarList(calresource, uri): #@UnusedVariable """ Store the calendars that match the query in L{fbset} which will then be used with the freebusy query. @param calresource: the L{CalDAVResource} for a calendar collection. @param uri: the uri for the calendar collection resource. """ fbset.append(calresource._newStoreObject) return succeed(True) # Run report taking depth into account depth = request.headers.getHeader("depth", "0") yield report_common.applyToCalendarCollections(self, request, request.uri, depth, getCalendarList, (caldavxml.ReadFreeBusy(),)) # Do the actual freebusy query against the set of matched calendars principal = yield self.resourceOwnerPrincipal(request) organizer = recipient = LocalCalendarUser(principal.canonicalCalendarUserAddress(), principal.record) timerange = Period(timerange.start, timerange.end) try: fbresult = yield FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange).generateAttendeeFreeBusyResponse(fbset=fbset, method=None) except NumberOfMatchesWithinLimits: log.error("Too many matching components in free-busy report") raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits(), "Too many components" )) except TimeRangeLowerLimit, e: raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, caldavxml.MinDateTime(), "Time-range value too far in the past. Must be on or after %s." % (str(e.limit),) ))
def report_urn_ietf_params_xml_ns_caldav_free_busy_query(self, request, freebusy): """ Generate a free-busy REPORT. (CalDAV-access-09, section 7.8) """ if not self.isCollection(): log.error("freebusy report is only allowed on collection resources {s!r}", s=self) raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Not a calendar collection")) if freebusy.qname() != (caldavxml.caldav_namespace, "free-busy-query"): raise ValueError("{CalDAV:}free-busy-query expected as root element, not %s." % (freebusy.sname(),)) timerange = freebusy.timerange if not timerange.valid(): raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid time-range specified")) fbset = [] accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes()) if accepted_type is None: raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type")) def getCalendarList(calresource, uri): # @UnusedVariable """ Store the calendars that match the query in L{fbset} which will then be used with the freebusy query. @param calresource: the L{CalDAVResource} for a calendar collection. @param uri: the uri for the calendar collection resource. """ fbset.append(calresource._newStoreObject) return succeed(True) # Run report taking depth into account depth = request.headers.getHeader("depth", "0") yield report_common.applyToCalendarCollections(self, request, request.uri, depth, getCalendarList, (caldavxml.ReadFreeBusy(),)) # Do the actual freebusy query against the set of matched calendars principal = yield self.resourceOwnerPrincipal(request) organizer = recipient = LocalCalendarUser(principal.canonicalCalendarUserAddress(), principal.record) timerange = Period(timerange.start, timerange.end) try: fbresult = yield FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange).generateAttendeeFreeBusyResponse(fbset=fbset, method=None) except NumberOfMatchesWithinLimits: log.error("Too many matching components in free-busy report") raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits(), "Too many components" )) except TimeRangeLowerLimit, e: raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, caldavxml.MinDateTime(), "Time-range value too far in the past. Must be on or after %s." % (str(e.limit),) ))
def report_urn_ietf_params_xml_ns_caldav_free_busy_query(self, request, freebusy): #@UnusedVariable """ Generate a free-busy REPORT. (CalDAV-access-09, section 7.8) """ if not self.isCollection(): log.err("freebusy report is only allowed on collection resources %s" % (self,)) raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Not a calendar collection")) if freebusy.qname() != (caldavxml.caldav_namespace, "free-busy-query"): raise ValueError("{CalDAV:}free-busy-query expected as root element, not %s." % (freebusy.sname(),)) timerange = freebusy.timerange if not timerange.valid(): raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid time-range specified")) # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE fbinfo = ([], [], []) matchcount = [0] def generateFreeBusyInfo(calresource, uri): #@UnusedVariable """ Run a free busy report on the specified calendar collection accumulating the free busy info for later processing. @param calresource: the L{CalDAVFile} for a calendar collection. @param uri: the uri for the calendar collecton resource. """ def _gotResult(result): matchcount[0] = result return True d = report_common.generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchcount[0]) d.addCallback(_gotResult) return d # Run report taking depth into account try: depth = request.headers.getHeader("depth", "0") yield report_common.applyToCalendarCollections(self, request, request.uri, depth, generateFreeBusyInfo, (caldavxml.ReadFreeBusy(),)) except NumberOfMatchesWithinLimits: log.err("Too many matching components in free-busy report") raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits())) # Now build a new calendar object with the free busy info we have fbcalendar = report_common.buildFreeBusyResult(fbinfo, timerange) response = Response() response.stream = MemoryStream(str(fbcalendar)) response.headers.setHeader("content-type", MimeType.fromString("text/calendar; charset=utf-8")) returnValue(response)
def getCalendarObjectForPrincipals(request, principal, uid, allow_shared=False): """ Get a copy of the event for a principal. """ result = { "resource": None, "resource_name": None, "calendar_collection": None, "calendar_collection_uri": None, } if principal and principal.locallyHosted(): # Get principal's calendar-home calendar_home = principal.calendarHome(request) # FIXME: because of the URL->resource request mapping thing, we have to force the request # to recognize this resource request._rememberResource(calendar_home, calendar_home.url()) # Run a UID query against the UID @inlineCallbacks def queryCalendarCollection(collection, uri): if not allow_shared: isvirt = (yield collection.isVirtualShare(request)) if isvirt: returnValue(True) rname = collection.index().resourceNameForUID(uid) if rname: resource = collection.getChild(rname) request._rememberResource(resource, joinURL(uri, rname)) result["resource"] = resource result["resource_name"] = rname result["calendar_collection"] = collection result["calendar_collection_uri"] = uri returnValue(False) else: returnValue(True) # NB We are by-passing privilege checking here. That should be OK as the data found is not # exposed to the user. yield report_common.applyToCalendarCollections(calendar_home, request, calendar_home.url(), "infinity", queryCalendarCollection, None) returnValue((result["resource"], result["resource_name"], result["calendar_collection"], result["calendar_collection_uri"],))
def hasCalendarResourceUIDSomewhereElse(self, check_resource, check_uri, type): """ See if a calendar component with a matching UID exists anywhere in the calendar home of the current recipient owner and is not the resource being targeted. """ # Don't care in some cases if self.internal_request or self.action == "remove": returnValue(None) # Get owner's calendar-home calendar_owner_principal = (yield self.resource.resourceOwnerPrincipal(self.request)) calendar_home = calendar_owner_principal.calendarHome(self.request) check_parent_uri = parentForURL(check_uri)[:-1] if check_uri else None # FIXME: because of the URL->resource request mapping thing, we have to force the request # to recognize this resource self.request._rememberResource(calendar_home, calendar_home.url()) # Run a UID query against the UID @inlineCallbacks def queryCalendarCollection(collection, collection_uri): rname = collection.index().resourceNameForUID(self.uid) if rname: child = (yield self.request.locateResource(joinURL(collection_uri, rname))) if child == check_resource: returnValue(True) is_scheduling_object = (yield self.checkSchedulingObjectResource(child)) matched_type = "schedule" if is_scheduling_object else "calendar" if ( collection_uri != check_parent_uri and (type == "schedule" or matched_type == "schedule") ): log.debug("Implicit - found component with same UID in a different collection: %s" % (check_uri,)) raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "unique-scheduling-object-resource"))) # Here we can always return true as the unique UID in a calendar collection # requirement will already have been tested. returnValue(True) # NB We are by-passing privilege checking here. That should be OK as the data found is not # exposed to the user. yield report_common.applyToCalendarCollections(calendar_home, self.request, calendar_home.url(), "infinity", queryCalendarCollection, None)
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 report_urn_ietf_params_xml_ns_caldav_free_busy_query( self, request, freebusy): #@UnusedVariable """ Generate a free-busy REPORT. (CalDAV-access-09, section 7.8) """ if not self.isCollection(): log.error( "freebusy report is only allowed on collection resources %s" % (self, )) raise HTTPError( StatusResponse(responsecode.FORBIDDEN, "Not a calendar collection")) if freebusy.qname() != (caldavxml.caldav_namespace, "free-busy-query"): raise ValueError( "{CalDAV:}free-busy-query expected as root element, not %s." % (freebusy.sname(), )) timerange = freebusy.timerange if not timerange.valid(): raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, "Invalid time-range specified")) # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE fbinfo = ([], [], []) matchcount = [0] accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes()) if accepted_type is None: raise HTTPError( StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type")) def generateFreeBusyInfo(calresource, uri): #@UnusedVariable """ Run a free busy report on the specified calendar collection accumulating the free busy info for later processing. @param calresource: the L{CalDAVResource} for a calendar collection. @param uri: the uri for the calendar collecton resource. """ def _gotResult(result): matchcount[0] = result return True d = report_common.generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchcount[0]) d.addCallback(_gotResult) return d # Run report taking depth into account try: depth = request.headers.getHeader("depth", "0") yield report_common.applyToCalendarCollections( self, request, request.uri, depth, generateFreeBusyInfo, (caldavxml.ReadFreeBusy(), )) except NumberOfMatchesWithinLimits: log.error("Too many matching components in free-busy report") raise HTTPError( ErrorResponse(responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits(), "Too many components")) except TimeRangeLowerLimit, e: raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, caldavxml.MinDateTime(), "Time-range value too far in the past. Must be on or after %s." % (str(e.limit), )))