Example #1
0
    def report_http___calendarserver_org_ns__calendarserver_principal_search(self, request,
        calendarserver_principal_search):
        """
        Generate a calendarserver-principal-search REPORT.

        @param request: Request object
        @param calendarserver_principal_search: CalendarServerPrincipalSearch object
        """

        # Verify root element
        if not isinstance(calendarserver_principal_search, customxml.CalendarServerPrincipalSearch):
            msg = "%s expected as root element, not %s." % (customxml.CalendarServerPrincipalSearch.sname(), calendarserver_principal_search.sname())
            log.warn(msg)
            raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))

        # Only handle Depth: 0
        depth = request.headers.getHeader("depth", "0")
        if depth != "0":
            log.error("Error in calendarserver-principal-search REPORT, Depth set to %s" % (depth,))
            raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth,)))

        tokens, context, applyTo, clientLimit, propElement = extractCalendarServerPrincipalSearchData(calendarserver_principal_search)

        if not validateTokens(tokens):
            raise HTTPError(StatusResponse(responsecode.FORBIDDEN,
                "Insufficient search token length"))

        # Run report
        resultsWereLimited = None
        resources = []
        if applyTo or not hasattr(self, "directory"):
            for principalCollection in self.principalCollections():
                uri = principalCollection.principalCollectionURL()
                resource = (yield request.locateResource(uri))
                if resource:
                    resources.append((resource, uri))
        else:
            resources.append((self, request.uri))

        # We need to access a directory service
        principalCollection = resources[0][0]
        dir = principalCollection.directory

        matchingResources = []
        matchcount = 0

        records = (yield dir.recordsMatchingTokens(tokens, context=context))

        for record in records:
            resource = principalCollection.principalForRecord(record)
            if resource:
                matchingResources.append(resource)

                # We've determined this is a matching resource
                matchcount += 1
                if clientLimit is not None and matchcount >= clientLimit:
                    resultsWereLimited = ("client", matchcount)
                    break
                if matchcount >= config.MaxPrincipalSearchReportResults:
                    resultsWereLimited = ("server", matchcount)
                    break

        # Generate the response
        responses = []
        for resource in matchingResources:
            url = resource.url()
            yield prop_common.responseForHref(
                request,
                responses,
                element.HRef.fromString(url),
                resource,
                prop_common.propertyListForResource,
                propElement
            )

        if resultsWereLimited is not None:
            if resultsWereLimited[0] == "server":
                log.error("Too many matching resources in calendarserver-principal-search report")
            responses.append(element.StatusResponse(
                element.HRef.fromString(request.uri),
                element.Status.fromResponseCode(
                    responsecode.INSUFFICIENT_STORAGE_SPACE
                ),
                element.Error(element.NumberOfMatchesWithinLimits()),
                element.ResponseDescription("Results limited by %s at %d"
                                           % resultsWereLimited),
            ))
        returnValue(MultiStatusResponse(responses))
def report_DAV__acl_principal_prop_set(self, request, acl_prinicpal_prop_set):
    """
    Generate an acl-prinicpal-prop-set REPORT. (RFC 3744, section 9.2)
    """
    # Verify root element
    if not isinstance(acl_prinicpal_prop_set, davxml.ACLPrincipalPropSet):
        raise ValueError("%s expected as root element, not %s."
                         % (davxml.ACLPrincipalPropSet.sname(), acl_prinicpal_prop_set.sname()))

    # Depth must be "0"
    depth = request.headers.getHeader("depth", "0")
    if depth != "0":
        log.error("Error in prinicpal-prop-set REPORT, Depth set to %s" % (depth,))
        raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth,)))
    
    #
    # Check authentication and access controls
    #
    x = waitForDeferred(self.authorize(request, (davxml.ReadACL(),)))
    yield x
    x.getResult()

    # Get a single DAV:prop element from the REPORT request body
    propertiesForResource = None
    propElement = None
    for child in acl_prinicpal_prop_set.children:
        if child.qname() == ("DAV:", "prop"):
            if propertiesForResource is not None:
                log.error("Only one DAV:prop element allowed")
                raise HTTPError(StatusResponse(
                    responsecode.BAD_REQUEST,
                    "Only one DAV:prop element allowed"
                ))
            propertiesForResource = prop_common.propertyListForResource
            propElement = child

    if propertiesForResource is None:
        log.error("Error in acl-principal-prop-set REPORT, no DAV:prop element")
        raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "No DAV:prop element"))

    # Enumerate principals on ACL in current resource
    principals = []

    acl = waitForDeferred(self.accessControlList(request))
    yield acl
    acl = acl.getResult()

    for ace in acl.children:
        resolved = waitForDeferred(self.resolvePrincipal(ace.principal.children[0], request))
        yield resolved
        resolved = resolved.getResult()
        if resolved is not None and resolved not in principals:
            principals.append(resolved)

    # Run report for each referenced principal
    try:
        responses = []
        matchcount = 0
        for principal in principals:
            # Check size of results is within limit
            matchcount += 1
            if matchcount > max_number_of_matches:
                raise NumberOfMatchesWithinLimits(max_number_of_matches)

            resource = waitForDeferred(request.locateResource(str(principal)))
            yield resource
            resource = resource.getResult()

            if resource is not None:
                #
                # Check authentication and access controls
                #
                x = waitForDeferred(resource.authorize(request, (davxml.Read(),)))
                yield x
                try:
                    x.getResult()
                except HTTPError:
                    responses.append(davxml.StatusResponse(
                        principal,
                        davxml.Status.fromResponseCode(responsecode.FORBIDDEN)
                    ))
                else:
                    d = waitForDeferred(prop_common.responseForHref(
                        request,
                        responses,
                        principal,
                        resource,
                        propertiesForResource,
                        propElement
                    ))
                    yield d
                    d.getResult()
            else:
                log.error("Requested principal resource not found: %s" % (str(principal),))
                responses.append(davxml.StatusResponse(
                    principal,
                    davxml.Status.fromResponseCode(responsecode.NOT_FOUND)
                ))

    except NumberOfMatchesWithinLimits:
        log.error("Too many matching components")
        raise HTTPError(ErrorResponse(
            responsecode.FORBIDDEN,
            davxml.NumberOfMatchesWithinLimits()
        ))

    yield MultiStatusResponse(responses)
def report_DAV__sync_collection(self, request, sync_collection):
    """
    Generate a sync-collection REPORT.
    """
    
    # These resource support the report
    if not config.EnableSyncReport or element.Report(element.SyncCollection(),) not in self.supportedReports():
        log.err("sync-collection report is only allowed on calendar/inbox/addressbook/notification collection resources %s" % (self,))
        raise HTTPError(ErrorResponse(
            responsecode.FORBIDDEN,
            element.SupportedReport(),
            "Report not supported on this resource",
        ))
   
    responses = []

    # Process Depth and sync-level for backwards compatibility
    # Use sync-level if present and ignore Depth, else use Depth
    if sync_collection.sync_level:
        depth = sync_collection.sync_level
        descriptor = "DAV:sync-level"
    else:
        depth = request.headers.getHeader("depth", None)
        descriptor = "Depth header without DAV:sync-level"
    
    if depth not in ("1", "infinity"):
        log.err("sync-collection report with invalid depth header: %s" % (depth,))
        raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid %s value" % (descriptor,)))
        
    propertyreq = sync_collection.property.children if sync_collection.property else None 
    
    @inlineCallbacks
    def _namedPropertiesForResource(request, props, resource, forbidden=False):
        """
        Return the specified properties on the specified resource.
        @param request: the L{IRequest} for the current request.
        @param props: a list of property elements or qname tuples for the properties of interest.
        @param resource: the L{DAVResource} for the targeted resource.
        @return: a map of OK and NOT FOUND property values.
        """
        properties_by_status = {
            responsecode.OK        : [],
            responsecode.FORBIDDEN : [],
            responsecode.NOT_FOUND : [],
        }
        
        for property in props:
            if isinstance(property, element.WebDAVElement):
                qname = property.qname()
            else:
                qname = property

            if forbidden:
                properties_by_status[responsecode.FORBIDDEN].append(propertyName(qname))
            else:
                props = (yield resource.listProperties(request))
                if qname in props:
                    try:
                        prop = (yield resource.readProperty(qname, request))
                        properties_by_status[responsecode.OK].append(prop)
                    except:
                        f = Failure()
                        log.err("Error reading property %r for resource %s: %s" % (qname, request.uri, f.value))
                        status = statusForFailure(f, "getting property: %s" % (qname,))
                        if status not in properties_by_status: properties_by_status[status] = []
                        properties_by_status[status].append(propertyName(qname))
                else:
                    properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname))
        
        returnValue(properties_by_status)
    
    # 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 self.inheritedACEsforChildren(request))

    changed, removed, notallowed, newtoken = yield self.whatchanged(sync_collection.sync_token, depth)

    # Now determine which valid resources are readable and which are not
    ok_resources = []
    forbidden_resources = []
    if changed:
        yield self.findChildrenFaster(
            depth,
            request,
            lambda x, y: ok_resources.append((x, y)),
            lambda x, y: forbidden_resources.append((x, y)),
            None,
            changed,
            (element.Read(),),
            inherited_aces=filteredaces
        )

    for child, child_uri in ok_resources:
        href = element.HRef.fromString(child_uri)
        try:
            yield responseForHref(
                request,
                responses,
                href,
                child,
                functools.partial(_namedPropertiesForResource, forbidden=False) if propertyreq else None,
                propertyreq)
        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.err("Missing resource during sync: %s" % (href,))

    for child, child_uri in forbidden_resources:
        href = element.HRef.fromString(child_uri)
        try:
            yield responseForHref(
                request,
                responses,
                href,
                child,
                functools.partial(_namedPropertiesForResource, forbidden=True) if propertyreq else None,
                propertyreq)
        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.err("Missing resource during sync: %s" % (href,))

    for name in removed:
        href = element.HRef.fromString(joinURL(request.uri, name))
        responses.append(element.StatusResponse(element.HRef.fromString(href), element.Status.fromResponseCode(responsecode.NOT_FOUND)))
    
    for name in notallowed:
        href = element.HRef.fromString(joinURL(request.uri, name))
        responses.append(element.StatusResponse(element.HRef.fromString(href), element.Status.fromResponseCode(responsecode.NOT_ALLOWED)))
    
    if not hasattr(request, "extendedLogItems"):
        request.extendedLogItems = {}
    request.extendedLogItems["responses"] = len(responses)

    responses.append(element.SyncToken.fromString(newtoken))

    returnValue(MultiStatusResponse(responses))
def report_DAV__principal_match(self, request, principal_match):
    """
    Generate a principal-match REPORT. (RFC 3744, section 9.3)
    """
    # Verify root element
    if not isinstance(principal_match, element.PrincipalMatch):
        raise ValueError("%s expected as root element, not %s."
                         % (element.PrincipalMatch.sname(), principal_match.sname()))

    # Only handle Depth: 0
    depth = request.headers.getHeader("depth", "0")
    if depth != "0":
        log.err("Non-zero depth is not allowed: %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
    principalPropElement = None
    lookForPrincipals = True

    for child in principal_match.children:
        if child.qname() == (dav_namespace, "prop"):
            propertiesForResource = prop_common.propertyListForResource
            propElement = child

        elif child.qname() == (dav_namespace, "self"):
            lookForPrincipals = True

        elif child.qname() == (dav_namespace, "principal-property"):
            # Must have one and only one property in this element
            if len(child.children) != 1:
                log.err("Wrong number of properties in DAV:principal-property: %s"
                        % (len(child.children),))
                raise HTTPError(StatusResponse(
                    responsecode.BAD_REQUEST,
                    "DAV:principal-property must contain exactly one property"
                ))

            lookForPrincipals = False
            principalPropElement = child.children[0]

    # Run report for each referenced principal
    try:
        responses = []
        matchcount = 0

        myPrincipalURL = self.currentPrincipal(request).children[0]

        if lookForPrincipals:

            # Find the set of principals that represent "self".
            
            # First add "self"
            principal = waitForDeferred(request.locateResource(str(myPrincipalURL)))
            yield principal
            principal = principal.getResult()
            selfItems = [principal,]
            
            # Get group memberships for "self" and add each of those
            d = waitForDeferred(principal.groupMemberships())
            yield d
            memberships = d.getResult()
            selfItems.extend(memberships)
            
            # Now add each principal found to the response provided the principal resource is a child of
            # the current resource.
            for principal in selfItems:
                # Get all the URIs that point to the principal resource
                # FIXME: making the assumption that the principalURL() is the URL of the resource we found
                principal_uris = [principal.principalURL()]
                principal_uris.extend(principal.alternateURIs())
                
                # Compare each one to the request URI and return at most one that matches
                for uri in principal_uris:
                    if uri.startswith(request.uri):
                        # 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),
                            principal,
                            propertiesForResource,
                            propElement
                        ))
                        yield d
                        d.getResult()
                        break
        else:
            # 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(self.inheritedACEsforChildren(request))
            yield filteredaces
            filteredaces = filteredaces.getResult()
        
            children = []
            d = waitForDeferred(self.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:
                # Try to read the requested property from this resource
                try:
                    prop = waitForDeferred(child.readProperty(principalPropElement.qname(), request))
                    yield prop
                    prop = prop.getResult()
                    if prop: prop.removeWhitespaceNodes()

                    if prop and len(prop.children) == 1 and isinstance(prop.children[0], element.HRef):
                        # Find principal associated with this property and test it
                        principal = waitForDeferred(request.locateResource(str(prop.children[0])))
                        yield principal
                        principal = principal.getResult()

                        if principal and isPrincipalResource(principal):
                            d = waitForDeferred(principal.principalMatch(myPrincipalURL))
                            yield d
                            matched = d.getResult()
                            if matched:
                                # 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 HTTPError:
                    # Just ignore a failure to access the property. We treat this like a property that does not exist
                    # or does not match the principal.
                    pass

    except NumberOfMatchesWithinLimits:
        log.err("Too many matching components in principal-match report")
        raise HTTPError(ErrorResponse(
            responsecode.FORBIDDEN,
            element.NumberOfMatchesWithinLimits()
        ))

    yield MultiStatusResponse(responses)
Example #5
0
def report_DAV__expand_property(self, request, expand_property):
    """
    Generate an expand-property REPORT. (RFC 3253, section 3.8)
    
    TODO: for simplicity we will only support one level of expansion.
    """
    # Verify root element
    if not isinstance(expand_property, davxml.ExpandProperty):
        raise ValueError("%s expected as root element, not %s."
                         % (davxml.ExpandProperty.sname(), expand_property.sname()))

    # Only handle Depth: 0
    depth = request.headers.getHeader("depth", "0")
    if depth != "0":
        log.err("Non-zero depth is not allowed: %s" % (depth,))
        raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth,)))
    
    #
    # Get top level properties to expand and make sure we only have one level
    #
    properties = {}

    for property in expand_property.children:
        namespace = property.attributes.get("namespace", dav_namespace)
        name      = property.attributes.get("name", "")
        
        # Make sure children have no children
        props_to_find = []
        for child in property.children:
            if child.children:
                log.err("expand-property REPORT only supports single level expansion")
                raise HTTPError(StatusResponse(
                    responsecode.NOT_IMPLEMENTED,
                    "expand-property REPORT only supports single level expansion"
                ))
            child_namespace = child.attributes.get("namespace", dav_namespace)
            child_name      = child.attributes.get("name", "")
            props_to_find.append((child_namespace, child_name))

        properties[(namespace, name)] = props_to_find

    #
    # Generate the expanded responses status for each top-level property
    #
    properties_by_status = {
        responsecode.OK        : [],
        responsecode.NOT_FOUND : [],
    }
    
    filteredaces = None
    lastParent = None

    for qname in properties.iterkeys():
        try:
            prop = (yield self.readProperty(qname, request))
            
            # Form the PROPFIND-style DAV:prop element we need later
            props_to_return = davxml.PropertyContainer(*properties[qname])

            # Now dereference any HRefs
            responses = []
            for href in prop.children:
                if isinstance(href, davxml.HRef):
                    
                    # Locate the Href resource and its parent
                    resource_uri = str(href)
                    child = (yield request.locateResource(resource_uri))
    
                    if not child or not child.exists():
                        responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
                        continue
                    parent = (yield request.locateResource(parentForURL(resource_uri)))
    
                    # Check privileges on parent - must have at least DAV:read
                    try:
                        yield parent.checkPrivileges(request, (davxml.Read(),))
                    except AccessDeniedError:
                        responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
                        continue
                    
                    # Cache the last parent's inherited aces for checkPrivileges optimization
                    if lastParent != parent:
                        lastParent = parent
                
                        # 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 = (yield parent.inheritedACEsforChildren(request))

                    # Check privileges - must have at least DAV:read
                    try:
                        yield child.checkPrivileges(request, (davxml.Read(),), inherited_aces=filteredaces)
                    except AccessDeniedError:
                        responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
                        continue
            
                    # Now retrieve all the requested properties on the HRef resource
                    yield prop_common.responseForHref(
                        request,
                        responses,
                        href,
                        child,
                        prop_common.propertyListForResource,
                        props_to_return,
                    )
            
            prop.children = responses
            properties_by_status[responsecode.OK].append(prop)
        except:
            f = Failure()

            log.err("Error reading property %r for resource %s: %s" % (qname, request.uri, f.value))

            status = statusForFailure(f, "getting property: %s" % (qname,))
            if status not in properties_by_status: properties_by_status[status] = []
            properties_by_status[status].append(propertyName(qname))

    # Build the overall response
    propstats = [
        davxml.PropertyStatus(
            davxml.PropertyContainer(*properties_by_status[status]),
            davxml.Status.fromResponseCode(status)
        )
        for status in properties_by_status if properties_by_status[status]
    ]

    returnValue(MultiStatusResponse((davxml.PropertyStatusResponse(davxml.HRef(request.uri), *propstats),)))
def report_DAV__sync_collection(self, request, sync_collection):
    """
    Generate a sync-collection REPORT.
    """
    if not self.isPseudoCalendarCollection() and not self.isAddressBookCollection() or not config.EnableSyncReport:
        log.err("sync-collection report is only allowed on calendar/inbox/addressbook collection resources %s" % (self,))
        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, davxml.SupportedReport()))
   
    responses = []

    propertyreq = sync_collection.property.children if sync_collection.property else None 
    
    @inlineCallbacks
    def _namedPropertiesForResource(request, props, resource, forbidden=False):
        """
        Return the specified properties on the specified resource.
        @param request: the L{IRequest} for the current request.
        @param props: a list of property elements or qname tuples for the properties of interest.
        @param resource: the L{DAVFile} for the targeted resource.
        @return: a map of OK and NOT FOUND property values.
        """
        properties_by_status = {
            responsecode.OK        : [],
            responsecode.FORBIDDEN : [],
            responsecode.NOT_FOUND : [],
        }
        
        for property in props:
            if isinstance(property, WebDAVElement):
                qname = property.qname()
            else:
                qname = property

            if forbidden:
                properties_by_status[responsecode.FORBIDDEN].append(propertyName(qname))
            else:
                props = (yield resource.listProperties(request))
                if qname in props:
                    try:
                        prop = (yield resource.readProperty(qname, request))
                        properties_by_status[responsecode.OK].append(prop)
                    except:
                        f = Failure()
                        log.err("Error reading property %r for resource %s: %s" % (qname, request.uri, f.value))
                        status = statusForFailure(f, "getting property: %s" % (qname,))
                        if status not in properties_by_status: properties_by_status[status] = []
                        properties_by_status[status].append(propertyName(qname))
                else:
                    properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname))
        
        returnValue(properties_by_status)
    
    # 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 self.inheritedACEsforChildren(request))

    changed, removed, newtoken = self.whatchanged(sync_collection.sync_token)

    # Now determine which valid resources are readable and which are not
    ok_resources = []
    forbidden_resources = []
    if changed:
        yield self.findChildrenFaster(
            "1",
            request,
            lambda x, y: ok_resources.append((x, y)),
            lambda x, y: forbidden_resources.append((x, y)),
            changed,
            (davxml.Read(),),
            inherited_aces=filteredaces
        )

    for child, child_uri in ok_resources:
        href = davxml.HRef.fromString(child_uri)
        yield responseForHref(
            request,
            responses,
            href,
            child,
            functools.partial(_namedPropertiesForResource, forbidden=False) if propertyreq else None,
            propertyreq)

    for child, child_uri in forbidden_resources:
        href = davxml.HRef.fromString(child_uri)
        yield responseForHref(
            request,
            responses,
            href,
            child,
            functools.partial(_namedPropertiesForResource, forbidden=True) if propertyreq else None,
            propertyreq)

    for name in removed:
        href = davxml.HRef.fromString(joinURL(request.uri, name))
        responses.append(davxml.StatusResponse(davxml.HRef.fromString(href), davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
    
    if not hasattr(request, "extendedLogItems"):
        request.extendedLogItems = {}
    request.extendedLogItems["responses"] = len(responses)

    responses.append(SyncToken.fromString(newtoken))

    returnValue(MultiStatusResponse(responses))
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, davxml.PrincipalPropertySearch):
        raise ValueError("%s expected as root element, not %s."
                         % (davxml.PrincipalPropertySearch.sname(), principal_property_search.sname()))

    # Only handle Depth: 0
    depth = request.headers.getHeader("depth", "0")
    if depth != "0":
        log.err("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(davxml.PropertyContainer)
            props.removeWhitespaceNodes()
            match = child.childOfType(davxml.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, davxml.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=(davxml.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,
                            davxml.HRef.fromString(uri),
                            child,
                            propertiesForResource,
                            propElement
                        ))
                        yield d
                        d.getResult()

    except NumberOfMatchesWithinLimits:
        log.err("Too many matching components in prinicpal-property-search report")
        raise HTTPError(ErrorResponse(
            responsecode.FORBIDDEN,
            davxml.NumberOfMatchesWithinLimits()
        ))

    yield MultiStatusResponse(responses)