def _gotResult(_): if not request.args: # Do normal GET behavior return self.render(request) method = request.args.get("method", ("", )) if len(method) != 1: raise HTTPError( ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-method"), "Invalid method query parameter", )) method = method[0] action = { "list": self.doPOSTList, "get": self.doPOSTGet, "expand": self.doPOSTExpand, }.get(method, None) if action is None: raise HTTPError( ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "supported-method"), "Unknown method query parameter", )) return action(request)
def loadCalendarFromRequest(self, request): # Must be content-type text/calendar contentType = request.headers.getHeader("content-type") format = self.determineType(contentType) if format is None: self.log.error("MIME type %s not allowed in calendar collection" % (contentType, )) raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data"), "Data is not calendar data", )) # Parse the calendar object from the HTTP request stream try: calendar = (yield Component.fromIStream(request.stream, format=format)) except: # FIXME: Bare except self.log.error("Error while handling POST: %s" % (Failure(), )) raise HTTPError( ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Can't parse calendar data")) returnValue(( calendar, format, ))
def acceptShare(self, request, inviteUID, summary): # Accept the share try: shareeView = yield self._newStoreHome.acceptShare( inviteUID, summary) except DirectoryRecordNotFoundError: # Missing sharer record => fail request raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, (calendarserver_namespace, "invalid-share"), "Invite UID not valid", )) if shareeView is None: raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, (calendarserver_namespace, "invalid-share"), "Invite UID not valid", )) # Return the URL of the shared collection sharedAsURL = joinURL(self.url(), shareeView.shareName()) returnValue( XMLResponse(code=responsecode.OK, element=customxml.SharedAs( element.HRef.fromString(sharedAsURL))))
def doPOSTGet(self, request): """ Return the specified timezone data. """ tzid = request.args.get("tzid", ()) if len(tzid) != 1: raise HTTPError( ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-timezone"), "Invalid tzid query parameter", )) tzid = tzid[0] try: tzdata = readTZ(tzid) except TimezoneException: raise HTTPError( ErrorResponse( responsecode.NOT_FOUND, (calendarserver_namespace, "timezone-available"), "Timezone not found", )) response = Response() response.stream = MemoryStream(tzdata) response.headers.setHeader( "content-type", MimeType.fromString("text/calendar; charset=utf-8")) return response
def loadCalendarFromRequest(self, request): # Must be content-type text/calendar contentType = request.headers.getHeader("content-type") if contentType is not None and (contentType.mediaType, contentType. mediaSubtype) != ("text", "calendar"): self.log.error( "MIME type {ct} not allowed in iSchedule POST request", ct=contentType, ) raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, (ischedule_namespace, "invalid-calendar-data-type"), "Data is not calendar data", )) # Parse the calendar object from the HTTP request stream try: calendar = (yield Component.fromIStream(request.stream)) except: # FIXME: Bare except self.log.error( "Error while handling iSchedule POST: {f}", f=Failure(), ) raise HTTPError( ErrorResponse(responsecode.FORBIDDEN, (ischedule_namespace, "invalid-calendar-data"), description="Can't parse calendar data")) returnValue(calendar)
def doPOSTExpand(self, request): """ Expand a timezone within specified start/end dates. """ tzid = request.args.get("tzid", ()) if len(tzid) != 1: raise HTTPError(ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-timezone"), "Invalid tzid query parameter", )) tzid = tzid[0] try: tzdata = readTZ(tzid) except TimezoneException: raise HTTPError(ErrorResponse( responsecode.NOT_FOUND, (calendarserver_namespace, "timezone-available"), "Timezone not found", )) try: start = request.args.get("start", ()) if len(start) != 1: raise ValueError() start = DateTime.parseText(start[0]) except ValueError: raise HTTPError(ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-start-date"), "Invalid start query parameter", )) try: end = request.args.get("end", ()) if len(end) != 1: raise ValueError() end = DateTime.parseText(end[0]) if end <= start: raise ValueError() except ValueError: raise HTTPError(ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-end-date"), "Invalid end query parameter", )) # Now do the expansion (but use a cache to avoid re-calculating TZs) observances = self.cache.get((tzid, start, end), None) if observances is None: observances = tzexpand(tzdata, start, end) self.cache[(tzid, start, end)] = observances # Turn into XML result = customxml.TZData( *[customxml.Observance(customxml.Onset(onset), customxml.UTCOffset(utc_offset)) for onset, utc_offset in observances] ) return XMLResponse(responsecode.OK, result)
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 writeProperty(self, property, request): assert isinstance(property, davxml.WebDAVElement) # Strictly speaking CS:calendar-availability is a live property in the sense that the # server enforces what can be stored, however it need not actually # exist so we cannot list it in liveProperties on this resource, since its # its presence there means that hasProperty will always return True for it. if property.qname() == customxml.CalendarAvailability.qname(): if not property.valid(): raise HTTPError(ErrorResponse( responsecode.CONFLICT, (caldav_namespace, "valid-calendar-data"), description="Invalid property" )) yield self.parent._newStoreHome.setAvailability(property.calendar()) returnValue(None) elif property.qname() == caldavxml.CalendarFreeBusySet.qname(): # Verify that the calendars added in the PROPPATCH are valid. We do not check # whether existing items in the property are still valid - only new ones. property.children = [davxml.HRef(normalizeURL(str(href))) for href in property.children] new_calendars = set([str(href) for href in property.children]) old_calendars = set() for cal in (yield self.parent._newStoreHome.calendars()): if cal.isUsedForFreeBusy(): old_calendars.add(HRef(joinURL(self.parent.url(), cal.name()))) added_calendars = new_calendars.difference(old_calendars) for href in added_calendars: cal = (yield request.locateResource(str(href))) if cal is None or not cal.exists() or not isCalendarCollectionResource(cal): # Validate that href's point to a valid calendar. raise HTTPError(ErrorResponse( responsecode.CONFLICT, (caldav_namespace, "valid-calendar-url"), "Invalid URI", )) # Remove old ones for href in old_calendars.difference(new_calendars): cal = (yield request.locateResource(str(href))) if cal is not None and cal.exists() and isCalendarCollectionResource(cal) and cal._newStoreObject.isUsedForFreeBusy(): yield cal._newStoreObject.setUsedForFreeBusy(False) # Add new ones for href in new_calendars: cal = (yield request.locateResource(str(href))) if cal is not None and cal.exists() and isCalendarCollectionResource(cal) and not cal._newStoreObject.isUsedForFreeBusy(): yield cal._newStoreObject.setUsedForFreeBusy(True) returnValue(None) elif property.qname() in (caldavxml.ScheduleDefaultCalendarURL.qname(), customxml.ScheduleDefaultTasksURL.qname()): yield self.writeDefaultCalendarProperty(request, property) returnValue(None) yield super(ScheduleInboxResource, self).writeProperty(property, request)
def writeDefaultCalendarProperty(self, request, property): """ Write either the default VEVENT or VTODO calendar property, validating and canonicalizing the value """ if property.qname() == caldavxml.ScheduleDefaultCalendarURL.qname(): ctype = "VEVENT" error_element = (caldav_namespace, "valid-schedule-default-calendar-URL") elif property.qname() == customxml.ScheduleDefaultTasksURL.qname(): ctype = "VTODO" error_element = (calendarserver_namespace, "valid-schedule-default-tasks-URL") else: returnValue(None) # Verify that the calendar added in the PROPPATCH is valid. property.children = [ davxml.HRef(normalizeURL(str(href))) for href in property.children ] new_calendar = [str(href) for href in property.children] cal = None if len(new_calendar) == 1: cal = (yield request.locateResource(str(new_calendar[0]))) else: raise HTTPError( ErrorResponse( responsecode.BAD_REQUEST, error_element, "Invalid HRef in property", )) if cal is None or not cal.exists(): raise HTTPError( ErrorResponse( responsecode.BAD_REQUEST, error_element, "HRef is not a valid calendar", )) try: # Now set it on the new store object yield self.parent._newStoreHome.setDefaultCalendar( cal._newStoreObject, ctype) except InvalidDefaultCalendar as e: raise HTTPError( ErrorResponse( responsecode.CONFLICT, error_element, str(e), ))
def _handleInviteReply(self, request, invitereplydoc): """ Handle a user accepting or declining a sharing invite """ hostUrl = None accepted = None summary = None replytoUID = None for item in invitereplydoc.children: if isinstance(item, customxml.InviteStatusAccepted): accepted = True elif isinstance(item, customxml.InviteStatusDeclined): accepted = False elif isinstance(item, customxml.InviteSummary): summary = str(item) elif isinstance(item, customxml.HostURL): for hosturlItem in item.children: if isinstance(hosturlItem, element.HRef): hostUrl = str(hosturlItem) elif isinstance(item, customxml.InReplyTo): replytoUID = str(item) if accepted is None or hostUrl is None or replytoUID is None: raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request"), "Missing required XML elements", )) if accepted: return self.acceptShare(request, replytoUID, summary=summary) else: return self.declineShare(request, replytoUID)
def _handleInviteSet(inviteset): userid = None cn = None access = None summary = None for item in inviteset.children: if isinstance(item, element.HRef): userid = str(item) continue if isinstance(item, customxml.CommonName): cn = str(item) continue if isinstance(item, customxml.InviteSummary): summary = str(item) continue if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess): access = item continue if userid and access and summary: return (userid, cn, access, summary) else: error_text = [] if userid is None: error_text.append("missing href") if access is None: error_text.append("missing access") if summary is None: error_text.append("missing summary") raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request"), "%s: %s" % (", ".join(error_text), inviteset,), ))
def loadOriginatorFromRequestDetails(self, request): # Get the originator who is the authenticated user originatorPrincipal = None originator = "" authz_principal = self.currentPrincipal(request).children[0] if isinstance(authz_principal, davxml.HRef): originatorPrincipalURL = str(authz_principal) if originatorPrincipalURL: originatorPrincipal = ( yield request.locateResource(originatorPrincipalURL)) if originatorPrincipal: # Pick the canonical CUA: originator = originatorPrincipal.canonicalCalendarUserAddress( ) if not originator: self.log.error("%s request must have Originator" % (self.method, )) raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, (caldav_namespace, "originator-specified"), "Missing originator", )) else: returnValue(originator)
def test_statusForFailure_HTTPError(self): """ statusForFailure() for HTTPErrors """ for code in responsecode.RESPONSES: self._check_exception(HTTPError(code), code) self._check_exception(HTTPError(ErrorResponse(code, ("http://twistedmatrix.com/", "bar"))), code)
def http_MKCALENDAR(self, request): """ Respond to a MKCALENDAR request. (CalDAV-access-09, section 5.3.1) """ # # Check authentication and access controls # parent = (yield request.locateResource(parentForURL(request.uri))) yield parent.authorize(request, (davxml.Bind(), )) if self.exists(): log.error("Attempt to create collection where resource exists: %s" % (self, )) raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, (davxml.dav_namespace, "resource-must-be-null"), "Resource already exists", )) if not parent.isCollection(): log.error( "Attempt to create collection with non-collection parent: %s" % (self, )) raise HTTPError( ErrorResponse( responsecode.CONFLICT, (caldavxml.caldav_namespace, "calendar-collection-location-ok"), "Cannot create calendar inside another calendar", )) # # Read request body # try: doc = (yield davXMLFromStream(request.stream)) yield self.createCalendar(request) except ValueError, e: log.error("Error while handling MKCALENDAR: %s" % (e, )) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
def _xmlHandleInviteReply(self, request, docroot): # Sharing must be enabled for this collection if not self.canShare(): raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request"), "Sharing not supported on this resource", )) yield self.authorize(request, (element.Read(), element.Write())) result = (yield self._handleInviteReply(request, docroot)) returnValue(result)
def doDirectoryAddressBookResponse(): directoryAddressBookLock = None try: # Verify that requested resources are immediate children of the request-URI # and get vCardFilters ;similar to "normal" case below but do not call getChild() vCardFilters = [] valid_hrefs = [] for href in resources: resource_uri = str(href) resource_name = unquote(resource_uri[resource_uri.rfind("/") + 1:]) if self._isChildURI(request, resource_uri) and resource_name.endswith(".vcf") and len(resource_name) > 4: valid_hrefs.append(href) textMatchElement = carddavxml.TextMatch.fromString(resource_name[:-4]) textMatchElement.attributes["match-type"] = "equals" # do equals compare. Default is "contains" vCardFilters.append(carddavxml.PropertyFilter( textMatchElement, name="UID", # attributes )) else: responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND))) # exit if not valid if not vCardFilters or not valid_hrefs: returnValue(None) addressBookFilter = carddavxml.Filter(*vCardFilters) addressBookFilter = Filter(addressBookFilter) # get vCards and filter limit = config.DirectoryAddressBook.MaxQueryResults results, limited = (yield self.doAddressBookDirectoryQuery(addressBookFilter, propertyreq, limit, defaultKind=None)) if limited: log.error("Too many results in multiget report: {count}", count=len(resources)) raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, (dav_namespace, "number-of-matches-within-limits"), "Too many results", )) for href in valid_hrefs: matchingResource = None for vCardResource in results: if href == vCardResource.hRef(): # might need to compare urls instead - also case sens ok? matchingResource = vCardResource break if matchingResource: yield report_common.responseForHref(request, responses, href, matchingResource, propertiesForResource, propertyreq, vcard=matchingResource.vCard()) else: responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND))) finally: if directoryAddressBookLock: yield directoryAddressBookLock.release()
def failForRecipient(recipient): err = HTTPError( ErrorResponse( responsecode.FORBIDDEN, (caldav_namespace, "recipient-failed"), "iMIP request failed", )) self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE, suppressErrorLog=True)
def xmlRequestHandler(self, request): # Need to read the data and get the root element first xmldata = (yield allDataFromStream(request.stream)) try: doc = element.WebDAVDocument.fromString(xmldata) except ValueError, e: self.log.error("Error parsing doc ({ex}) Doc:\n {x}", ex=str(e), x=xmldata) raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request"), "Invalid XML", ))
def loadOriginatorFromRequestHeaders(self, request): # Must have Originator header originator = request.headers.getRawHeaders("originator") if originator is None or (len(originator) != 1): self.log.error("iSchedule POST request must have Originator header") raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, (ischedule_namespace, "originator-missing"), "Missing originator", )) else: originator = originator[0] return originator
def declineShare(self, request, inviteUID): # Remove it if it is in the DB try: result = yield self._newStoreHome.declineShare(inviteUID) except DirectoryRecordNotFoundError: # Missing sharer record => just treat decline as success result = True if not result: raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, (calendarserver_namespace, "invalid-share"), "Invite UID not valid", )) returnValue(Response(code=responsecode.NO_CONTENT))
def generateFreeBusyResponse(self, recipient, responses, organizerProp, organizerPrincipal, uid, event_details): # Extract the ATTENDEE property matching current recipient from the calendar data cuas = recipient.record.calendarUserAddresses attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas) remote = isinstance(self.scheduler.organizer, RemoteCalendarUser) try: fbresult = (yield self.generateAttendeeFreeBusyResponse( recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote, event_details, )) except Exception: log.failure( "Could not determine free busy information for recipient {cuaddr}", cuaddr=recipient.cuaddr, level=LogLevel.debug ) log.error( "Could not determine free busy information for recipient {cuaddr}", cuaddr=recipient.cuaddr ) err = HTTPError(ErrorResponse( responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions"), "Could not determine free busy information", )) responses.add( recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_AUTHORITY ) returnValue(False) else: responses.add( recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.SUCCESS, calendar=fbresult ) returnValue(True)
def generateFreeBusyResponse(self, recipient, responses, organizerProp, uid, event_details): # Extract the ATTENDEE property matching current recipient from the calendar data cuas = recipient.record.calendarUserAddresses attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas) try: fbresult = yield FreebusyQuery( organizer=self.scheduler.organizer, organizerProp=organizerProp, recipient=recipient, attendeeProp=attendeeProp, uid=uid, timerange=self.scheduler.timeRange, excludeUID=self.scheduler.excludeUID, logItems=self.scheduler.logItems, event_details=event_details, ).generateAttendeeFreeBusyResponse() except Exception as e: log.failure( "Could not determine free busy information for recipient {cuaddr}", cuaddr=recipient.cuaddr, level=LogLevel.debug ) log.error( "Could not determine free busy information for recipient {cuaddr}: {ex}", cuaddr=recipient.cuaddr, ex=e ) err = HTTPError(ErrorResponse( responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions"), "Could not determine free busy information", )) responses.add( recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_AUTHORITY ) returnValue(False) else: responses.add( recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.SUCCESS, calendar=fbresult ) returnValue(True)
def loadOriginatorFromRequestDetails(self, request): # The originator is the owner of the Outbox. We will have checked prior to this # that the authenticated user has privileges to schedule as the owner. originator = "" originatorPrincipal = (yield self.ownerPrincipal(request)) if originatorPrincipal: # Pick the canonical CUA: originator = originatorPrincipal.canonicalCalendarUserAddress() if not originator: self.log.error("{m} request must have Originator", m=self.method) raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, (caldav_namespace, "originator-specified"), "Missing originator", )) else: returnValue(originator)
def loadRecipientsFromCalendarData(self, calendar): # Get the ATTENDEEs attendees = list() unique_set = set() for attendee, _ignore in calendar.getAttendeesByInstance(): if attendee not in unique_set: attendees.append(attendee) unique_set.add(attendee) if not attendees: self.log.error("POST request must have at least one ATTENDEE") raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified"), "Must have recipients", )) else: return(list(attendees))
def _handleInviteRemove(inviteremove): userid = None access = [] for item in inviteremove.children: if isinstance(item, element.HRef): userid = str(item) continue if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess): access.append(item) continue if userid is None: raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request"), "Missing href: %s" % (inviteremove,), )) if len(access) == 0: access = None else: access = set(access) return (userid, access)
def loadRecipientsFromRequestHeaders(self, request): # Get list of Recipient headers rawRecipients = request.headers.getRawHeaders("recipient") if rawRecipients is None or (len(rawRecipients) == 0): self.log.error( "{method} request must have at least one Recipient header", method=self.method, ) raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, (ischedule_namespace, "recipient-missing"), "No recipients", )) # Recipient header may be comma separated list recipients = [] for rawRecipient in rawRecipients: for r in rawRecipient.split(","): r = r.strip() if len(r): recipients.append(r) return recipients
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", ))
# 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", )) except NumberOfMatchesWithinLimits: log.error("Too many matching components in calendar-query 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),) )) except TimeRangeUpperLimit, e: raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, caldavxml.MaxDateTime(), "Time-range value too far in the future. Must be on or before %s." % (str(e.limit),) ))
error = "Request XML body is required." log.error("Error: {err}", err=error) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, error)) # # Parse request # acl = doc.root_element if not isinstance(acl, davxml.ACL): error = ("Request XML body must be an acl element." % (davxml.PropertyUpdate.sname(),)) log.error("Error: {err}", err=error) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, error)) # # Do ACL merger # result = waitForDeferred(self.mergeAccessControlList(acl, request)) yield result result = result.getResult() # # Return response # if result is None: yield responsecode.OK else: yield ErrorResponse(responsecode.FORBIDDEN, result) http_ACL = deferredGenerator(http_ACL)
def report_DAV__principal_property_search(self, request, principal_property_search): """ Generate a principal-property-search REPORT. (RFC 3744, section 9.4) """ # Verify root element if not isinstance(principal_property_search, element.PrincipalPropertySearch): raise ValueError("%s expected as root element, not %s." % (element.PrincipalPropertySearch.sname(), principal_property_search.sname())) # Only handle Depth: 0 depth = request.headers.getHeader("depth", "0") if depth != "0": log.error("Error in prinicpal-property-search REPORT, Depth set to %s" % (depth,)) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth,))) # Get a single DAV:prop element from the REPORT request body propertiesForResource = None propElement = None propertySearches = [] applyTo = False for child in principal_property_search.children: if child.qname() == (dav_namespace, "prop"): propertiesForResource = prop_common.propertyListForResource propElement = child elif child.qname() == (dav_namespace, "apply-to-principal-collection-set"): applyTo = True elif child.qname() == (dav_namespace, "property-search"): props = child.childOfType(element.PropertyContainer) props.removeWhitespaceNodes() match = child.childOfType(element.Match) propertySearches.append((props.children, str(match).lower())) def nodeMatch(node, match): """ See if the content of the supplied node matches the supplied text. Try to follow the matching guidance in rfc3744 section 9.4.1. @param prop: the property element to match. @param match: the text to match against. @return: True if the property matches, False otherwise. """ node.removeWhitespaceNodes() for child in node.children: if isinstance(child, PCDATAElement): comp = str(child).lower() if comp.find(match) != -1: return True else: return nodeMatch(child, match) else: return False def propertySearch(resource, request): """ Test the resource to see if it contains properties matching the property-search specification in this report. @param resource: the L{DAVFile} for the resource to test. @param request: the current request. @return: True if the resource has matching properties, False otherwise. """ for props, match in propertySearches: # Test each property for prop in props: try: propvalue = waitForDeferred(resource.readProperty(prop.qname(), request)) yield propvalue propvalue = propvalue.getResult() if propvalue and not nodeMatch(propvalue, match): yield False return except HTTPError: # No property => no match yield False return yield True propertySearch = deferredGenerator(propertySearch) # Run report try: resources = [] responses = [] matchcount = 0 if applyTo: for principalCollection in self.principalCollections(): uri = principalCollection.principalCollectionURL() resource = waitForDeferred(request.locateResource(uri)) yield resource resource = resource.getResult() if resource: resources.append((resource, uri)) else: resources.append((self, request.uri)) # Loop over all collections and principal resources within for resource, ruri in resources: # Do some optimisation 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 = waitForDeferred(resource.inheritedACEsforChildren(request)) yield filteredaces filteredaces = filteredaces.getResult() children = [] d = waitForDeferred(resource.findChildren("infinity", request, lambda x, y: children.append((x,y)), privileges=(element.Read(),), inherited_aces=filteredaces)) yield d d.getResult() for child, uri in children: if isPrincipalResource(child): d = waitForDeferred(propertySearch(child, request)) yield d d = d.getResult() if d: # Check size of results is within limit matchcount += 1 if matchcount > max_number_of_matches: raise NumberOfMatchesWithinLimits(max_number_of_matches) d = waitForDeferred(prop_common.responseForHref( request, responses, element.HRef.fromString(uri), child, propertiesForResource, propElement )) yield d d.getResult() except NumberOfMatchesWithinLimits: log.error("Too many matching components in prinicpal-property-search report") raise HTTPError(ErrorResponse( responsecode.FORBIDDEN, element.NumberOfMatchesWithinLimits() )) yield MultiStatusResponse(responses)