예제 #1
0
    def contains(self, qname, uid=None):
        """
        Determine whether the property given by C{qname} is stored in an
        extended attribute of the wrapped path.

        @param qname: The property to look up as a two-tuple of namespace URI
            and local name.

        @param uid: The per-user identifier for per user properties.

        @return: C{True} if the property exists, C{False} otherwise.
        """
            
        key = self._encode(qname, uid)
        try:
            self.attrs.get(key)
        except KeyError:
            return False
        except IOError, e:
            if e.errno in _ATTR_MISSING or e.errno == errno.ENOENT:
                return False
            raise HTTPError(StatusResponse(
                statusForFailure(Failure()),
                "Unable to read property: %s" % (key,)
            ))
예제 #2
0
    def get(self, qname, uid=None):
        """
        Retrieve the value of a property stored as an extended attribute on the
        wrapped path.

        @param qname: The property to retrieve as a two-tuple of namespace URI
            and local name.

        @param uid: The per-user identifier for per user properties.

        @raise HTTPError: If there is no value associated with the given
            property.

        @return: A L{WebDAVDocument} representing the value associated with the
            given property.
        """
        
        try:
            data = self.attrs.get(self._encode(qname, uid))
        except KeyError:
            raise HTTPError(StatusResponse(
                responsecode.NOT_FOUND,
                "No such property: %s" % (encodeXMLName(*qname),)
            ))
        except IOError, e:
            if e.errno in _ATTR_MISSING or e.errno == errno.ENOENT:
                raise HTTPError(StatusResponse(
                    responsecode.NOT_FOUND,
                    "No such property: %s" % (encodeXMLName(*qname),)
                ))
            else:
                raise HTTPError(StatusResponse(
                    statusForFailure(Failure()),
                    "Unable to read property: %s" % (encodeXMLName(*qname),)
                ))
예제 #3
0
 def _check_exception(self, exception, result):
     try:
         raise exception
     except Exception:
         failure = Failure()
         status = statusForFailure(failure)
         self.failUnless(
             status == result,
             "Failure %r (%s) generated incorrect status code: %s != %s" %
             (failure, failure.value, status, result))
     else:
         raise AssertionError("We shouldn't be here.")
예제 #4
0
 def _check_exception(self, exception, result):
     try:
         raise exception
     except Exception:
         failure = Failure()
         status = statusForFailure(failure)
         self.failUnless(
             status == result,
             "Failure %r (%s) generated incorrect status code: %s != %s"
             % (failure, failure.value, status, result)
         )
     else:
         raise AssertionError("We shouldn't be here.")
예제 #5
0
    def add(self,
            recipient,
            what,
            reqstatus=None,
            calendar=None,
            suppressErrorLog=False):
        """
        Add a response.
        @param recipient: the recipient for this response.
        @param what: a status code or a L{Failure} for the given recipient.
        @param status: the iTIP request-status for the given recipient.
        @param calendar: the calendar data for the given recipient response.
        @param suppressErrorLog: whether to suppress a log message for errors; primarily
            this is used when trying to process a VFREEBUSY over iMIP, which isn't
            supported.
        """
        if type(what) is int:
            code = what
            error = None
            message = responsecode.RESPONSES[code]
        elif isinstance(what, Failure):
            code = statusForFailure(what)
            error = self.errorForFailure(what)
            message = messageForFailure(what)
        else:
            raise AssertionError("Unknown data type: {}".format(what, ))

        if self.recipient_mapper is not None:
            recipient = self.recipient_mapper(recipient)

        if not suppressErrorLog and code > 400:  # Error codes only
            self.log.error(
                "Error during {method} for {r}: {msg}",
                method=self.method,
                r=recipient,
                msg=message,
            )

        details = ScheduleResponseQueue.ScheduleResonseDetails(
            self.recipient_element(davxml.HRef.fromString(recipient))
            if self.recipient_uses_href else
            self.recipient_element.fromString(recipient),
            self.request_status_element(reqstatus),
            calendar,
            error,
            self.response_description_element(message)
            if message is not None else None,
        )
        self.responses.append(details)
    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.error(
                            "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)
예제 #7
0
    def list(self, uid=None, filterByUID=True):
        """
        Enumerate the property names stored in extended attributes of the
        wrapped path.

        @param uid: The per-user identifier for per user properties.

        @return: A C{list} of property names as two-tuples of namespace URI and
            local name.
        """

        prefix = self.deadPropertyXattrPrefix
        try:
            attrs = iter(self.attrs)
        except IOError, e:
            if e.errno == errno.ENOENT:
                return []
            raise HTTPError(
                StatusResponse(statusForFailure(Failure()), "Unable to list properties: %s", (self.resource.fp.path,))
            )
예제 #8
0
def mkcollection(filepath):
    """
    Perform a X{MKCOL} on the given filepath.
    @param filepath: the L{FilePath} of the collection resource to create.
    @raise HTTPError: (containing an appropriate response) if the operation
        fails.
    @return: a deferred response with a status code of L{responsecode.CREATED}
        if the destination already exists, or L{responsecode.NO_CONTENT} if the
        destination was created by the X{MKCOL} operation.
    """
    try:
        os.mkdir(filepath.path)
        # Remove stat info from filepath because we modified it
        filepath.changed()
    except:
        raise HTTPError(statusForFailure(
            Failure(),
            "creating directory in MKCOL: %s" % (filepath.path,)
        ))

    return succeed(responsecode.CREATED)
예제 #9
0
    def delete(self, qname, uid=None):
        """
        Remove the extended attribute from the wrapped path which stores the
        property given by C{qname}.

        @param uid: The per-user identifier for per user properties.

        @param qname: The property to delete as a two-tuple of namespace URI
            and local name.
        """
        key = self._encode(qname, uid)
        try:
            try:
                self.attrs.remove(key)
            except KeyError:
                pass
            except IOError, e:
                if e.errno not in _ATTR_MISSING:
                    raise
        except:
            raise HTTPError(StatusResponse(statusForFailure(Failure()), "Unable to delete property: %s", (key,)))
예제 #10
0
    def list(self, uid=None, filterByUID=True):
        """
        Enumerate the property names stored in extended attributes of the
        wrapped path.

        @param uid: The per-user identifier for per user properties.

        @return: A C{list} of property names as two-tuples of namespace URI and
            local name.
        """

        prefix = self.deadPropertyXattrPrefix
        try:
            attrs = iter(self.attrs)
        except IOError, e:
            if e.errno == errno.ENOENT:
                return []
            raise HTTPError(
                StatusResponse(statusForFailure(Failure()),
                               "Unable to list properties: %s",
                               (self.resource.fp.path, )))
예제 #11
0
def mkcollection(filepath):
    """
    Perform a X{MKCOL} on the given filepath.
    @param filepath: the L{FilePath} of the collection resource to create.
    @raise HTTPError: (containing an appropriate response) if the operation
        fails.
    @return: a deferred response with a status code of L{responsecode.CREATED}
        if the destination already exists, or L{responsecode.NO_CONTENT} if the
        destination was created by the X{MKCOL} operation.
    """
    try:
        os.mkdir(filepath.path)
        # Remove stat info from filepath because we modified it
        filepath.changed()
    except:
        raise HTTPError(
            statusForFailure(
                Failure(),
                "creating directory in MKCOL: %s" % (filepath.path, )))

    return succeed(responsecode.CREATED)
예제 #12
0
    def add(self, recipient, what, reqstatus=None, calendar=None, suppressErrorLog=False):
        """
        Add a response.
        @param recipient: the recipient for this response.
        @param what: a status code or a L{Failure} for the given recipient.
        @param status: the iTIP request-status for the given recipient.
        @param calendar: the calendar data for the given recipient response.
        @param suppressErrorLog: whether to suppress a log message for errors; primarily
            this is used when trying to process a VFREEBUSY over iMIP, which isn't
            supported.
        """
        if type(what) is int:
            code = what
            error = None
            message = responsecode.RESPONSES[code]
        elif isinstance(what, Failure):
            code = statusForFailure(what)
            error = self.errorForFailure(what)
            message = messageForFailure(what)
        else:
            raise AssertionError("Unknown data type: {}".format(what,))

        if self.recipient_mapper is not None:
            recipient = self.recipient_mapper(recipient)

        if not suppressErrorLog and code > 400: # Error codes only
            self.log.error("Error during {method} for {r}: {msg}",
                method=self.method,
                r=recipient,
                msg=message,
            )

        details = ScheduleResponseQueue.ScheduleResonseDetails(
            self.recipient_element(davxml.HRef.fromString(recipient)) if self.recipient_uses_href else self.recipient_element.fromString(recipient),
            self.request_status_element(reqstatus),
            calendar,
            error,
            self.response_description_element(message) if message is not None else None,
        )
        self.responses.append(details)
예제 #13
0
def _namedPropertiesForResource(request, props, resource):
    """
    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 targetted resource.
    @return: a map of OK and NOT FOUND property values.
    """
    properties_by_status = {
        responsecode.OK        : [],
        responsecode.NOT_FOUND : [],
    }
    
    for property in props:
        if isinstance(property, element.WebDAVElement):
            qname = property.qname()
        else:
            qname = property
    
        props = waitForDeferred(resource.listProperties(request))
        yield props
        props = props.getResult()
        if qname in props:
            try:
                prop = waitForDeferred(resource.readProperty(qname, request))
                yield prop
                prop = prop.getResult()
                properties_by_status[responsecode.OK].append(prop)
            except:
                f = Failure()
                status = statusForFailure(f, "getting property: %s" % (qname,))
                if status != responsecode.NOT_FOUND:
                    log.error("Error reading property %r for resource %s: %s" %
                              (qname, request.uri, f.value))
                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))
    
    yield properties_by_status
예제 #14
0
    def delete(self, qname, uid=None):
        """
        Remove the extended attribute from the wrapped path which stores the
        property given by C{qname}.

        @param uid: The per-user identifier for per user properties.

        @param qname: The property to delete as a two-tuple of namespace URI
            and local name.
        """
        key = self._encode(qname, uid)
        try:
            try:
                self.attrs.remove(key)
            except KeyError:
                pass
            except IOError, e:
                if e.errno not in _ATTR_MISSING:
                    raise
        except:
            raise HTTPError(
                StatusResponse(statusForFailure(Failure()),
                               "Unable to delete property: %s", (key, )))
예제 #15
0
    def get(self, qname, uid=None):
        """
        Retrieve the value of a property stored as an extended attribute on the
        wrapped path.

        @param qname: The property to retrieve as a two-tuple of namespace URI
            and local name.

        @param uid: The per-user identifier for per user properties.

        @raise HTTPError: If there is no value associated with the given
            property.

        @return: A L{WebDAVDocument} representing the value associated with the
            given property.
        """

        try:
            data = self.attrs.get(self._encode(qname, uid))
        except KeyError:
            raise HTTPError(
                StatusResponse(
                    responsecode.NOT_FOUND,
                    "No such property: %s" % (encodeXMLName(*qname), )))
        except IOError, e:
            if e.errno in _ATTR_MISSING or e.errno == errno.ENOENT:
                raise HTTPError(
                    StatusResponse(
                        responsecode.NOT_FOUND,
                        "No such property: %s" % (encodeXMLName(*qname), )))
            else:
                raise HTTPError(
                    StatusResponse(
                        statusForFailure(Failure()),
                        "Unable to read property: %s" %
                        (encodeXMLName(*qname), )))
예제 #16
0
    def contains(self, qname, uid=None):
        """
        Determine whether the property given by C{qname} is stored in an
        extended attribute of the wrapped path.

        @param qname: The property to look up as a two-tuple of namespace URI
            and local name.

        @param uid: The per-user identifier for per user properties.

        @return: C{True} if the property exists, C{False} otherwise.
        """

        key = self._encode(qname, uid)
        try:
            self.attrs.get(key)
        except KeyError:
            return False
        except IOError, e:
            if e.errno in _ATTR_MISSING or e.errno == errno.ENOENT:
                return False
            raise HTTPError(
                StatusResponse(statusForFailure(Failure()),
                               "Unable to read property: %s" % (key, )))
예제 #17
0
def copy(source_filepath, destination_filepath, destination_uri, depth):
    """
    Perform a X{COPY} from the given source and destination filepaths.
    This will perform a X{DELETE} on the destination if necessary; the caller
    should check and handle the X{overwrite} header before calling L{copy} (as
    in L{COPYMOVE.prepareForCopy}).
    @param source_filepath: a L{FilePath} for the file to copy from.
    @param destination_filepath: a L{FilePath} for the file to copy to.
    @param destination_uri: the URI of the destination resource.
    @param depth: the recursion X{Depth} for the X{COPY} operation, which must
        be one of "0", "1", or "infinity".
    @raise HTTPError: (containing a response with a status code of
        L{responsecode.BAD_REQUEST}) if C{depth} is not "0", "1" or "infinity".
    @raise HTTPError: (containing an appropriate response) if the operation
        fails.  If C{source_filepath} is a directory, the response will be a
        L{MultiStatusResponse}.
    @return: a deferred response with a status code of L{responsecode.CREATED}
        if the destination already exists, or L{responsecode.NO_CONTENT} if the
        destination was created by the X{COPY} operation.
    """
    if source_filepath.isfile():
        #
        # Copy the file
        #
        log.info("Copying file %s to %s" % (source_filepath.path, destination_filepath.path))

        try:
            source_file = source_filepath.open()
        except:
            raise HTTPError(statusForFailure(
                Failure(),
                "opening file for reading: %s" % (source_filepath.path,)
            ))
    
        source_stream = FileStream(source_file)
        response = waitForDeferred(put(source_stream, destination_filepath, destination_uri))
        yield response
        try:
            response = response.getResult()
        finally:
            source_stream.close()
            source_file.close()
        checkResponse(response, "put", responsecode.NO_CONTENT, responsecode.CREATED)
        yield response
        return

    elif source_filepath.isdir():
        if destination_filepath.exists():
            #
            # Delete the destination
            #
            response = waitForDeferred(delete(destination_uri, destination_filepath))
            yield response
            response = response.getResult()
            checkResponse(response, "delete", responsecode.NO_CONTENT)
            success_code = responsecode.NO_CONTENT
        else:
            success_code = responsecode.CREATED

        #
        # Copy the directory
        #
        log.info("Copying directory %s to %s" % (source_filepath.path, destination_filepath.path))

        source_basename = source_filepath.path
        destination_basename = destination_filepath.path

        errors = ResponseQueue(source_basename, "COPY", success_code)

        if destination_filepath.parent().isdir():
            if os.path.islink(source_basename):
                link_destination = os.readlink(source_basename)
                if link_destination[0] != os.path.sep:
                    link_destination = os.path.join(source_basename, link_destination)
                try:
                    os.symlink(destination_basename, link_destination)
                except:
                    errors.add(source_basename, Failure())
            else:
                try:
                    os.mkdir(destination_basename)
                except:
                    raise HTTPError(statusForFailure(
                        Failure(),
                        "creating directory %s" % (destination_basename,)
                    ))

                if depth == "0": 
                    yield success_code
                    return
        else:
            raise HTTPError(StatusResponse(
                responsecode.CONFLICT,
                "Parent collection for destination %s does not exist" % (destination_uri,)
            ))

        #
        # Recursive copy
        #
        # FIXME: When we report errors, do we report them on the source URI
        # or on the destination URI?  We're using the source URI here.
        #
        # FIXME: defer the walk?

        source_basename_len = len(source_basename)

        def paths(basepath, subpath):
            source_path = os.path.join(basepath, subpath)
            assert source_path.startswith(source_basename)
            destination_path = os.path.join(destination_basename, source_path[source_basename_len+1:])
            return source_path, destination_path

        for dir, subdirs, files in os.walk(source_filepath.path, topdown=True):
            for filename in files:
                source_path, destination_path = paths(dir, filename)
                if not os.path.isdir(os.path.dirname(destination_path)):
                    errors.add(source_path, responsecode.NOT_FOUND)
                else:
                    response = waitForDeferred(copy(FilePath(source_path), FilePath(destination_path), destination_uri, depth))
                    yield response
                    response = response.getResult()
                    checkResponse(response, "copy", responsecode.CREATED, responsecode.NO_CONTENT)

            for subdir in subdirs:
                source_path, destination_path = paths(dir, subdir)
                log.info("Copying directory %s to %s" % (source_path, destination_path))

                if not os.path.isdir(os.path.dirname(destination_path)):
                    errors.add(source_path, responsecode.CONFLICT)
                else:
                    if os.path.islink(source_path):
                        link_destination = os.readlink(source_path)
                        if link_destination[0] != os.path.sep:
                            link_destination = os.path.join(source_path, link_destination)
                        try:
                            os.symlink(destination_path, link_destination)
                        except:
                            errors.add(source_path, Failure())
                    else:
                        try:
                            os.mkdir(destination_path)
                        except:
                            errors.add(source_path, Failure())

        yield errors.response()
        return
    else:
        log.error("Unable to COPY to non-file: %s" % (source_filepath.path,))
        raise HTTPError(StatusResponse(
            responsecode.FORBIDDEN,
            "The requested resource exists but is not backed by a regular file."
        ))
예제 #18
0
def _namedPropertiesForResource(request, props, resource, calendar=None, timezone=None, vcard=None, isowner=True, dataAllowed=True, 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{CalDAVResource} for the targeted resource.
    @param calendar: the L{Component} for the calendar for the resource. This may be None
        if the calendar has not already been read in, in which case the resource
        will be used to get the calendar if needed.
    @param timezone: the L{Component} the VTIMEZONE to use for floating/all-day.
    @param vcard: the L{Component} for the vcard for the resource. This may be None
        if the vcard has not already been read in, in which case the resource
        will be used to get the vcard if needed.
    @param isowner: C{True} if the authorized principal making the request is the DAV:owner,
        C{False} otherwise.
    @param dataAllowed: C{True} if calendar/address data is allowed to be returned,
        C{False} otherwise.
    @param forbidden: if C{True} then return 403 status for all properties,
        C{False} otherwise.
    @return: a map of OK and NOT FOUND property values.
    """
    properties_by_status = {
        responsecode.OK        : [],
        responsecode.FORBIDDEN : [],
        responsecode.NOT_FOUND : [],
    }

    # Look for Prefer header first, then try Brief
    prefer = request.headers.getHeader("prefer", {})
    returnMinimal = any([key == "return" and value == "minimal" for key, value, _ignore_args in prefer])
    if not returnMinimal:
        returnMinimal = request.headers.getHeader("brief", False)

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

        if isinstance(property, caldavxml.CalendarData):
            if dataAllowed:
                # Handle private events access restrictions
                if calendar is None:
                    calendar = (yield resource.componentForUser())
                filtered = HiddenInstanceFilter().filter(calendar)
                filtered = PrivateEventFilter(resource.accessMode, isowner).filter(filtered)
                filtered = CalendarDataFilter(property, timezone).filter(filtered)
                propvalue = CalendarData.fromCalendar(filtered, format=property.content_type)
                properties_by_status[responsecode.OK].append(propvalue)
            else:
                properties_by_status[responsecode.FORBIDDEN].append(propertyName(qname))
            continue

        if isinstance(property, carddavxml.AddressData):
            if dataAllowed:
                if vcard is None:
                    vcard = (yield resource.vCard())
                filtered = AddressDataFilter(property).filter(vcard)
                propvalue = AddressData.fromAddress(filtered, format=property.content_type)
                properties_by_status[responsecode.OK].append(propvalue)
            else:
                properties_by_status[responsecode.FORBIDDEN].append(propertyName(qname))
            continue

        has = (yield resource.hasProperty(property, request))

        if has:
            try:
                prop = (yield resource.readProperty(property, request))
                if prop is not None:
                    properties_by_status[responsecode.OK].append(prop)
                elif not returnMinimal:
                    properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname))
            except HTTPError:
                f = Failure()
                status = statusForFailure(f, "getting property: %s" % (qname,))
                if status not in properties_by_status:
                    properties_by_status[status] = []
                if not returnMinimal or status != responsecode.NOT_FOUND:
                    properties_by_status[status].append(propertyName(qname))
        elif not returnMinimal:
            properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname))

    returnValue(properties_by_status)
예제 #19
0
def _namedPropertiesForResource(request, props, resource, calendar=None, timezone=None, vcard=None, isowner=True):
    """
    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{CalDAVResource} for the targeted resource.
    @param calendar: the L{Component} for the calendar for the resource. This may be None
        if the calendar has not already been read in, in which case the resource
        will be used to get the calendar if needed.
    @param timezone: the L{Component} the VTIMEZONE to use for floating/all-day.
    @param vcard: the L{Component} for the vcard for the resource. This may be None
        if the vcard has not already been read in, in which case the resource
        will be used to get the vcard if needed.
    @param isowner: C{True} if the authorized principal making the request is the DAV:owner,
        C{False} otherwise.
    @return: a map of OK and NOT FOUND property values.
    """
    properties_by_status = {
        responsecode.OK        : [],
        responsecode.NOT_FOUND : [],
    }

    # Look for Prefer header first, then try Brief
    prefer = request.headers.getHeader("prefer", {})
    returnMinimal = any([key == "return" and value == "minimal" for key, value, _ignore_args in prefer])
    if not returnMinimal:
        returnMinimal = request.headers.getHeader("brief", False)

    for property in props:
        if isinstance(property, caldavxml.CalendarData):
            # Handle private events access restrictions
            if calendar is None:
                calendar = (yield resource.iCalendarForUser(request))
            filtered = HiddenInstanceFilter().filter(calendar)
            filtered = PrivateEventFilter(resource.accessMode, isowner).filter(filtered)
            filtered = CalendarDataFilter(property, timezone).filter(filtered)
            propvalue = CalendarData.fromCalendar(filtered, format=property.content_type)
            properties_by_status[responsecode.OK].append(propvalue)
            continue

        if isinstance(property, carddavxml.AddressData):
            if vcard is None:
                vcard = (yield resource.vCard())
            filtered = AddressDataFilter(property).filter(vcard)
            propvalue = AddressData.fromAddress(filtered, format=property.content_type)
            properties_by_status[responsecode.OK].append(propvalue)
            continue

        if isinstance(property, element.WebDAVElement):
            qname = property.qname()
        else:
            qname = property

        has = (yield resource.hasProperty(property, request))

        if has:
            try:
                prop = (yield resource.readProperty(property, request))
                if prop is not None:
                    properties_by_status[responsecode.OK].append(prop)
                elif not returnMinimal:
                    properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname))
            except HTTPError:
                f = Failure()
                status = statusForFailure(f, "getting property: %s" % (qname,))
                if status not in properties_by_status:
                        properties_by_status[status] = []
                if not returnMinimal or status != responsecode.NOT_FOUND:
                    properties_by_status[status].append(propertyName(qname))
        elif not returnMinimal:
            properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname))

    returnValue(properties_by_status)
예제 #20
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, element.ExpandProperty):
        raise ValueError(
            "%s expected as root element, not %s." %
            (element.ExpandProperty.sname(), expand_property.sname()))

    # Only handle Depth: 0
    depth = request.headers.getHeader("depth", "0")
    if depth != "0":
        log.error("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.error(
                    "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 = element.PropertyContainer(*properties[qname])

            # Now dereference any HRefs
            responses = []
            for href in prop.children:
                if isinstance(href, element.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(
                            element.StatusResponse(
                                href,
                                element.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,
                                                     (element.Read(), ))
                    except AccessDeniedError:
                        responses.append(
                            element.StatusResponse(
                                href,
                                element.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, (element.Read(), ),
                            inherited_aces=filteredaces)
                    except AccessDeniedError:
                        responses.append(
                            element.StatusResponse(
                                href,
                                element.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.error(
                "Error reading property {qname} for resource {req}: {failure}",
                qname=qname,
                req=request.uri,
                failure=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 = [
        element.PropertyStatus(
            element.PropertyContainer(*properties_by_status[pstatus]),
            element.Status.fromResponseCode(pstatus))
        for pstatus in properties_by_status if properties_by_status[pstatus]
    ]

    returnValue(
        MultiStatusResponse(
            (element.PropertyStatusResponse(element.HRef(request.uri),
                                            *propstats), )))
예제 #21
0
 if hasattr(resource, "hasProperty"):
     has = waitForDeferred(
         resource.hasProperty(property, request))
     yield has
     has = has.getResult()
 else:
     has = False
 if has:
     try:
         resource_property = waitForDeferred(
             resource.readProperty(property, request))
         yield resource_property
         resource_property = resource_property.getResult()
     except:
         f = Failure()
         status = statusForFailure(
             f, "getting property: %s" % (property, ))
         if status not in properties_by_status:
             properties_by_status[status] = []
         if not returnMinimal or status != responsecode.NOT_FOUND:
             properties_by_status[status].append(
                 propertyName(property))
     else:
         if resource_property is not None:
             properties_by_status[responsecode.OK].append(
                 resource_property)
         elif not returnMinimal:
             properties_by_status[
                 responsecode.NOT_FOUND].append(
                     propertyName(property))
 elif not returnMinimal:
     properties_by_status[responsecode.NOT_FOUND].append(
예제 #22
0
def delete(uri, filepath, depth="infinity"):
    """
    Perform a X{DELETE} operation on the given URI, which is backed by the given
    filepath.
    @param filepath: the L{FilePath} to delete.
    @param depth: the recursion X{Depth} for the X{DELETE} operation, which must
        be "infinity".
    @raise HTTPError: (containing a response with a status code of
        L{responsecode.BAD_REQUEST}) if C{depth} is not "infinity".
    @raise HTTPError: (containing an appropriate response) if the
        delete operation fails.  If C{filepath} is a directory, the response
        will be a L{MultiStatusResponse}.
    @return: a deferred response with a status code of L{responsecode.NO_CONTENT}
        if the X{DELETE} operation succeeds.
    """
    #
    # Remove the file(s)
    #
    # FIXME: defer
    if filepath.isdir():
        #
        # RFC 2518, section 8.6 says that we must act as if the Depth header is
        # set to infinity, and that the client must omit the Depth header or set
        # it to infinity, meaning that for collections, we will delete all
        # members.
        #
        # This seems somewhat at odds with the notion that a bad request should
        # be rejected outright; if the client sends a bad depth header, the
        # client is broken, and RFC 2518, section 8 suggests that a bad request
        # should be rejected...
        #
        # Let's play it safe for now and ignore broken clients.
        #

        if depth != "infinity":
            msg = ("Client sent illegal depth header value for DELETE: %s" %
                   (depth, ))
            log.error(msg)
            raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))

        #
        # Recursive delete
        #
        # RFC 2518, section 8.6 says that if we get an error deleting a resource
        # other than the collection in the request-URI, that we must respond
        # with a multi-status response containing error statuses for each
        # resource that we fail to delete.  It also says we should not return
        # no-content (success) status, which means that we should continue after
        # errors, rather than aborting right away.  This is interesting in that
        # it's different from how most operating system tools act (eg. rm) when
        # recursive filsystem deletes fail.
        #

        uri_path = urllib.unquote(urlsplit(uri)[2])
        if uri_path[-1] == "/":
            uri_path = uri_path[:-1]

        log.info("Deleting directory %s" % (filepath.path, ))

        # NOTE: len(uri_path) is wrong if os.sep is not one byte long... meh.
        request_basename = filepath.path[:-len(uri_path)]

        errors = ResponseQueue(request_basename, "DELETE",
                               responsecode.NO_CONTENT)

        # FIXME: defer this
        for dir, subdirs, files in os.walk(filepath.path, topdown=False):
            for filename in files:
                path = os.path.join(dir, filename)
                try:
                    os.remove(path)
                except:
                    errors.add(path, Failure())

            for subdir in subdirs:
                path = os.path.join(dir, subdir)
                if os.path.islink(path):
                    try:
                        os.remove(path)
                    except:
                        errors.add(path, Failure())
                else:
                    try:
                        os.rmdir(path)
                    except:
                        errors.add(path, Failure())

        try:
            os.rmdir(filepath.path)
        except:
            raise HTTPError(
                statusForFailure(Failure(),
                                 "deleting directory: %s" % (filepath.path, )))

        response = errors.response()

    else:
        #
        # Delete a file; much simpler, eh?
        #
        log.info("Deleting file %s" % (filepath.path, ))
        try:
            os.remove(filepath.path)
        except:
            raise HTTPError(
                statusForFailure(Failure(),
                                 "deleting file: %s" % (filepath.path, )))

        response = responsecode.NO_CONTENT

    # Remove stat info for filepath since we deleted the backing file
    filepath.changed()

    return succeed(response)
예제 #23
0
def put(stream, filepath, uri=None):
    """
    Perform a PUT of the given data stream into the given filepath.
    @param stream: the stream to write to the destination.
    @param filepath: the L{FilePath} of the destination file.
    @param uri: the URI of the destination resource.
        If the destination exists, if C{uri} is not C{None}, perform a
        X{DELETE} operation on the destination, but if C{uri} is C{None},
        delete the destination directly.
        Note that whether a L{put} deletes the destination directly vs.
        performing a X{DELETE} on the destination affects the response returned
        in the event of an error during deletion.  Specifically, X{DELETE}
        on collections must return a L{MultiStatusResponse} under certain
        circumstances, whereas X{PUT} isn't required to do so.  Therefore,
        if the caller expects X{DELETE} semantics, it must provide a valid
        C{uri}.
    @raise HTTPError: (containing an appropriate response) if the operation
        fails.
    @return: a deferred response with a status code of L{responsecode.CREATED}
        if the destination already exists, or L{responsecode.NO_CONTENT} if the
        destination was created by the X{PUT} operation.
    """
    log.info("Writing to file %s" % (filepath.path, ))

    if filepath.exists():
        if uri is None:
            try:
                if filepath.isdir():
                    rmdir(filepath.path)
                else:
                    os.remove(filepath.path)
            except:
                raise HTTPError(
                    statusForFailure(Failure(), "writing to file: %s" %
                                     (filepath.path, )))
        else:
            response = waitForDeferred(delete(uri, filepath))
            yield response
            response = response.getResult()
            checkResponse(response, "delete", responsecode.NO_CONTENT)

        success_code = responsecode.NO_CONTENT
    else:
        success_code = responsecode.CREATED

    #
    # Write the contents of the request stream to resource's file
    #

    try:
        resource_file = filepath.open("w")
    except:
        raise HTTPError(
            statusForFailure(
                Failure(), "opening file for writing: %s" % (filepath.path, )))

    try:
        x = waitForDeferred(readIntoFile(stream, resource_file))
        yield x
        x.getResult()
    except:
        raise HTTPError(
            statusForFailure(Failure(),
                             "writing to file: %s" % (filepath.path, )))

    # Remove stat info from filepath since we modified the backing file
    filepath.changed()
    yield success_code
예제 #24
0
def copy(source_filepath, destination_filepath, destination_uri, depth):
    """
    Perform a X{COPY} from the given source and destination filepaths.
    This will perform a X{DELETE} on the destination if necessary; the caller
    should check and handle the X{overwrite} header before calling L{copy} (as
    in L{COPYMOVE.prepareForCopy}).
    @param source_filepath: a L{FilePath} for the file to copy from.
    @param destination_filepath: a L{FilePath} for the file to copy to.
    @param destination_uri: the URI of the destination resource.
    @param depth: the recursion X{Depth} for the X{COPY} operation, which must
        be one of "0", "1", or "infinity".
    @raise HTTPError: (containing a response with a status code of
        L{responsecode.BAD_REQUEST}) if C{depth} is not "0", "1" or "infinity".
    @raise HTTPError: (containing an appropriate response) if the operation
        fails.  If C{source_filepath} is a directory, the response will be a
        L{MultiStatusResponse}.
    @return: a deferred response with a status code of L{responsecode.CREATED}
        if the destination already exists, or L{responsecode.NO_CONTENT} if the
        destination was created by the X{COPY} operation.
    """
    if source_filepath.isfile():
        #
        # Copy the file
        #
        log.info("Copying file %s to %s" %
                 (source_filepath.path, destination_filepath.path))

        try:
            source_file = source_filepath.open()
        except:
            raise HTTPError(
                statusForFailure(
                    Failure(),
                    "opening file for reading: %s" % (source_filepath.path, )))

        source_stream = FileStream(source_file)
        response = waitForDeferred(
            put(source_stream, destination_filepath, destination_uri))
        yield response
        try:
            response = response.getResult()
        finally:
            source_stream.close()
            source_file.close()
        checkResponse(response, "put", responsecode.NO_CONTENT,
                      responsecode.CREATED)
        yield response
        return

    elif source_filepath.isdir():
        if destination_filepath.exists():
            #
            # Delete the destination
            #
            response = waitForDeferred(
                delete(destination_uri, destination_filepath))
            yield response
            response = response.getResult()
            checkResponse(response, "delete", responsecode.NO_CONTENT)
            success_code = responsecode.NO_CONTENT
        else:
            success_code = responsecode.CREATED

        #
        # Copy the directory
        #
        log.info("Copying directory %s to %s" %
                 (source_filepath.path, destination_filepath.path))

        source_basename = source_filepath.path
        destination_basename = destination_filepath.path

        errors = ResponseQueue(source_basename, "COPY", success_code)

        if destination_filepath.parent().isdir():
            if os.path.islink(source_basename):
                link_destination = os.readlink(source_basename)
                if link_destination[0] != os.path.sep:
                    link_destination = os.path.join(source_basename,
                                                    link_destination)
                try:
                    os.symlink(destination_basename, link_destination)
                except:
                    errors.add(source_basename, Failure())
            else:
                try:
                    os.mkdir(destination_basename)
                except:
                    raise HTTPError(
                        statusForFailure(
                            Failure(), "creating directory %s" %
                            (destination_basename, )))

                if depth == "0":
                    yield success_code
                    return
        else:
            raise HTTPError(
                StatusResponse(
                    responsecode.CONFLICT,
                    "Parent collection for destination %s does not exist" %
                    (destination_uri, )))

        #
        # Recursive copy
        #
        # FIXME: When we report errors, do we report them on the source URI
        # or on the destination URI?  We're using the source URI here.
        #
        # FIXME: defer the walk?

        source_basename_len = len(source_basename)

        def paths(basepath, subpath):
            source_path = os.path.join(basepath, subpath)
            assert source_path.startswith(source_basename)
            destination_path = os.path.join(
                destination_basename, source_path[source_basename_len + 1:])
            return source_path, destination_path

        for dir, subdirs, files in os.walk(source_filepath.path, topdown=True):
            for filename in files:
                source_path, destination_path = paths(dir, filename)
                if not os.path.isdir(os.path.dirname(destination_path)):
                    errors.add(source_path, responsecode.NOT_FOUND)
                else:
                    response = waitForDeferred(
                        copy(FilePath(source_path), FilePath(destination_path),
                             destination_uri, depth))
                    yield response
                    response = response.getResult()
                    checkResponse(response, "copy", responsecode.CREATED,
                                  responsecode.NO_CONTENT)

            for subdir in subdirs:
                source_path, destination_path = paths(dir, subdir)
                log.info("Copying directory %s to %s" %
                         (source_path, destination_path))

                if not os.path.isdir(os.path.dirname(destination_path)):
                    errors.add(source_path, responsecode.CONFLICT)
                else:
                    if os.path.islink(source_path):
                        link_destination = os.readlink(source_path)
                        if link_destination[0] != os.path.sep:
                            link_destination = os.path.join(
                                source_path, link_destination)
                        try:
                            os.symlink(destination_path, link_destination)
                        except:
                            errors.add(source_path, Failure())
                    else:
                        try:
                            os.mkdir(destination_path)
                        except:
                            errors.add(source_path, Failure())

        yield errors.response()
        return
    else:
        log.error("Unable to COPY to non-file: %s" % (source_filepath.path, ))
        raise HTTPError(
            StatusResponse(
                responsecode.FORBIDDEN,
                "The requested resource exists but is not backed by a regular file."
            ))
예제 #25
0
def put(stream, filepath, uri=None):
    """
    Perform a PUT of the given data stream into the given filepath.
    @param stream: the stream to write to the destination.
    @param filepath: the L{FilePath} of the destination file.
    @param uri: the URI of the destination resource.
        If the destination exists, if C{uri} is not C{None}, perform a
        X{DELETE} operation on the destination, but if C{uri} is C{None},
        delete the destination directly.
        Note that whether a L{put} deletes the destination directly vs.
        performing a X{DELETE} on the destination affects the response returned
        in the event of an error during deletion.  Specifically, X{DELETE}
        on collections must return a L{MultiStatusResponse} under certain
        circumstances, whereas X{PUT} isn't required to do so.  Therefore,
        if the caller expects X{DELETE} semantics, it must provide a valid
        C{uri}.
    @raise HTTPError: (containing an appropriate response) if the operation
        fails.
    @return: a deferred response with a status code of L{responsecode.CREATED}
        if the destination already exists, or L{responsecode.NO_CONTENT} if the
        destination was created by the X{PUT} operation.
    """
    log.info("Writing to file %s" % (filepath.path,))

    if filepath.exists():
        if uri is None:
            try:
                if filepath.isdir():
                    rmdir(filepath.path)
                else:
                    os.remove(filepath.path)
            except:
                raise HTTPError(statusForFailure(
                    Failure(),
                    "writing to file: %s" % (filepath.path,)
                ))
        else:
            response = waitForDeferred(delete(uri, filepath))
            yield response
            response = response.getResult()
            checkResponse(response, "delete", responsecode.NO_CONTENT)

        success_code = responsecode.NO_CONTENT
    else:
        success_code = responsecode.CREATED

    #
    # Write the contents of the request stream to resource's file
    #

    try:
        resource_file = filepath.open("w")
    except:
        raise HTTPError(statusForFailure(
            Failure(),
            "opening file for writing: %s" % (filepath.path,)
        ))

    try:
        x = waitForDeferred(readIntoFile(stream, resource_file))
        yield x
        x.getResult()
    except:
        raise HTTPError(statusForFailure(
            Failure(),
            "writing to file: %s" % (filepath.path,)
        ))

    # Remove stat info from filepath since we modified the backing file
    filepath.changed()
    yield success_code
예제 #26
0
def delete(uri, filepath, depth="infinity"):
    """
    Perform a X{DELETE} operation on the given URI, which is backed by the given
    filepath.
    @param filepath: the L{FilePath} to delete.
    @param depth: the recursion X{Depth} for the X{DELETE} operation, which must
        be "infinity".
    @raise HTTPError: (containing a response with a status code of
        L{responsecode.BAD_REQUEST}) if C{depth} is not "infinity".
    @raise HTTPError: (containing an appropriate response) if the
        delete operation fails.  If C{filepath} is a directory, the response
        will be a L{MultiStatusResponse}.
    @return: a deferred response with a status code of L{responsecode.NO_CONTENT}
        if the X{DELETE} operation succeeds.
    """
    #
    # Remove the file(s)
    #
    # FIXME: defer
    if filepath.isdir():
        #
        # RFC 2518, section 8.6 says that we must act as if the Depth header is
        # set to infinity, and that the client must omit the Depth header or set
        # it to infinity, meaning that for collections, we will delete all
        # members.
        #
        # This seems somewhat at odds with the notion that a bad request should
        # be rejected outright; if the client sends a bad depth header, the
        # client is broken, and RFC 2518, section 8 suggests that a bad request
        # should be rejected...
        #
        # Let's play it safe for now and ignore broken clients.
        #

        if depth != "infinity":
            msg = ("Client sent illegal depth header value for DELETE: %s" % (depth,))
            log.error(msg)
            raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))

        #
        # Recursive delete
        #
        # RFC 2518, section 8.6 says that if we get an error deleting a resource
        # other than the collection in the request-URI, that we must respond
        # with a multi-status response containing error statuses for each
        # resource that we fail to delete.  It also says we should not return
        # no-content (success) status, which means that we should continue after
        # errors, rather than aborting right away.  This is interesting in that
        # it's different from how most operating system tools act (eg. rm) when
        # recursive filsystem deletes fail.
        #

        uri_path = urllib.unquote(urlsplit(uri)[2])
        if uri_path[-1] == "/":
            uri_path = uri_path[:-1]

        log.info("Deleting directory %s" % (filepath.path,))

        # NOTE: len(uri_path) is wrong if os.sep is not one byte long... meh.
        request_basename = filepath.path[:-len(uri_path)]

        errors = ResponseQueue(request_basename, "DELETE", responsecode.NO_CONTENT)

        # FIXME: defer this
        for dir, subdirs, files in os.walk(filepath.path, topdown=False):
            for filename in files:
                path = os.path.join(dir, filename)
                try:
                    os.remove(path)
                except:
                    errors.add(path, Failure())

            for subdir in subdirs:
                path = os.path.join(dir, subdir)
                if os.path.islink(path):
                    try:
                        os.remove(path)
                    except:
                        errors.add(path, Failure())
                else:
                    try:
                        os.rmdir(path)
                    except:
                        errors.add(path, Failure())

        try:
            os.rmdir(filepath.path)
        except:
            raise HTTPError(statusForFailure(
                Failure(),
                "deleting directory: %s" % (filepath.path,)
            ))

        response = errors.response()

    else:
        #
        # Delete a file; much simpler, eh?
        #
        log.info("Deleting file %s" % (filepath.path,))
        try:
            os.remove(filepath.path)
        except:
            raise HTTPError(statusForFailure(
                Failure(),
                "deleting file: %s" % (filepath.path,)
            ))

        response = responsecode.NO_CONTENT

    # Remove stat info for filepath since we deleted the backing file
    filepath.changed()

    return succeed(response)
예제 #27
0
                    responsecode.NOT_FOUND : [],
                }

                if search_properties == "all":
                    properties_to_enumerate = (yield resource.listAllprop(request))
                else:
                    properties_to_enumerate = search_properties

                for property in properties_to_enumerate:
                    has = (yield resource.hasProperty(property, request))
                    if has:
                        try:
                            resource_property = (yield resource.readProperty(property, request))
                        except:
                            f = Failure()
                            status = statusForFailure(f, "getting property: %s" % (property,))
                            if status not in properties_by_status:
                                properties_by_status[status] = []
                            if not returnMinimal or status != responsecode.NOT_FOUND:
                                properties_by_status[status].append(propertyName(property))
                        else:
                            if resource_property is not None:
                                properties_by_status[responsecode.OK].append(resource_property)
                            elif not returnMinimal and search_properties != "all":
                                properties_by_status[responsecode.NOT_FOUND].append(propertyName(property))
                    elif not returnMinimal and search_properties != "all":
                        properties_by_status[responsecode.NOT_FOUND].append(propertyName(property))

            propstats = []

            for status in properties_by_status:
예제 #28
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, element.ExpandProperty):
        raise ValueError("%s expected as root element, not %s."
                         % (element.ExpandProperty.sname(), expand_property.sname()))

    # Only handle Depth: 0
    depth = request.headers.getHeader("depth", "0")
    if depth != "0":
        log.error("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.error("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 = element.PropertyContainer(*properties[qname])

            # Now dereference any HRefs
            responses = []
            for href in prop.children:
                if isinstance(href, element.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(element.StatusResponse(href, element.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, (element.Read(),))
                    except AccessDeniedError:
                        responses.append(element.StatusResponse(href, element.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, (element.Read(),), inherited_aces=filteredaces)
                    except AccessDeniedError:
                        responses.append(element.StatusResponse(href, element.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.error(
                "Error reading property {qname} for resource {req}: {failure}",
                qname=qname, req=request.uri, failure=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 = [
        element.PropertyStatus(
            element.PropertyContainer(*properties_by_status[pstatus]),
            element.Status.fromResponseCode(pstatus)
        )
        for pstatus in properties_by_status if properties_by_status[pstatus]
    ]

    returnValue(MultiStatusResponse((element.PropertyStatusResponse(element.HRef(request.uri), *propstats),)))