Exemple #1
0
 def match(item, filter_):
     tag = collection_tag
     if (tag == "VCALENDAR" and
             filter_.tag != xmlutils.make_tag("C", filter_)):
         if len(filter_) == 0:
             return True
         if len(filter_) > 1:
             raise ValueError("Filter with %d children" % len(filter_))
         if filter_[0].tag != xmlutils.make_tag("C", "comp-filter"):
             raise ValueError("Unexpected %r in filter" % filter_[0].tag)
         return radicale_filter.comp_match(item, filter_[0])
     if (tag == "VADDRESSBOOK" and
             filter_.tag != xmlutils.make_tag("CR", filter_)):
         for child in filter_:
             if child.tag != xmlutils.make_tag("CR", "prop-filter"):
                 raise ValueError("Unexpected %r in filter" % child.tag)
         test = filter_.get("test", "anyof")
         if test == "anyof":
             return any(
                 radicale_filter.prop_match(item.vobject_item, f, "CR")
                 for f in filter_)
         if test == "allof":
             return all(
                 radicale_filter.prop_match(item.vobject_item, f, "CR")
                 for f in filter_)
         raise ValueError("Unsupported filter test: %r" % test)
         return all(radicale_filter.prop_match(item.vobject_item, f, "CR")
                    for f in filter_)
     raise ValueError("unsupported filter %r for %r" % (filter_.tag, tag))
Exemple #2
0
def prop_match(vobject_item, filter_, ns):
    """Check whether the ``item`` matches the prop ``filter_``.

    See rfc4791-9.7.2 and rfc6352-10.5.1.

    """
    name = filter_.get("name").lower()
    if len(filter_) == 0:
        # Point #1 of rfc4791-9.7.2
        return name in vobject_item.contents
    if len(filter_) == 1:
        if filter_[0].tag == xmlutils.make_tag("C", "is-not-defined"):
            # Point #2 of rfc4791-9.7.2
            return name not in vobject_item.contents
    if name not in vobject_item.contents:
        return False
    # Point #3 and #4 of rfc4791-9.7.2
    for child in filter_:
        if ns == "C" and child.tag == xmlutils.make_tag("C", "time-range"):
            if not time_range_match(vobject_item, child, name):
                return False
        elif child.tag == xmlutils.make_tag(ns, "text-match"):
            if not text_match(vobject_item, child, name, ns):
                return False
        elif child.tag == xmlutils.make_tag(ns, "param-filter"):
            if not param_filter_match(vobject_item, child, name, ns):
                return False
        else:
            raise ValueError("Unexpected %r in prop-filter" % child.tag)
    return True
Exemple #3
0
 def match(item, filter_):
     tag = collection_tag
     if (tag == "VCALENDAR" and
             filter_.tag != xmlutils.make_tag("C", filter_)):
         if len(filter_) == 0:
             return True
         if len(filter_) > 1:
             raise ValueError("Filter with %d children" % len(filter_))
         if filter_[0].tag != xmlutils.make_tag("C", "comp-filter"):
             raise ValueError("Unexpected %r in filter" % filter_[0].tag)
         return radicale_filter.comp_match(item, filter_[0])
     if (tag == "VADDRESSBOOK" and
             filter_.tag != xmlutils.make_tag("CR", filter_)):
         for child in filter_:
             if child.tag != xmlutils.make_tag("CR", "prop-filter"):
                 raise ValueError("Unexpected %r in filter" % child.tag)
         test = filter_.get("test", "anyof")
         if test == "anyof":
             return any(
                 radicale_filter.prop_match(item.vobject_item, f, "CR")
                 for f in filter_)
         if test == "allof":
             return all(
                 radicale_filter.prop_match(item.vobject_item, f, "CR")
                 for f in filter_)
         raise ValueError("Unsupported filter test: %r" % test)
         return all(radicale_filter.prop_match(item.vobject_item, f, "CR")
                    for f in filter_)
     raise ValueError("unsupported filter %r for %r" % (filter_.tag, tag))
Exemple #4
0
def xml_proppatch(base_prefix, path, xml_request, collection):
    """Read and answer PROPPATCH requests.

    Read rfc4918-9.2 for info.

    """
    props_to_set = xmlutils.props_from_request(xml_request, actions=("set",))
    props_to_remove = xmlutils.props_from_request(xml_request,
                                                  actions=("remove",))

    multistatus = ET.Element(xmlutils.make_tag("D", "multistatus"))
    response = ET.Element(xmlutils.make_tag("D", "response"))
    multistatus.append(response)

    href = ET.Element(xmlutils.make_tag("D", "href"))
    href.text = xmlutils.make_href(base_prefix, path)
    response.append(href)

    new_props = collection.get_meta()
    for short_name, value in props_to_set.items():
        new_props[short_name] = value
        xml_add_propstat_to(response, short_name, 200)
    for short_name in props_to_remove:
        try:
            del new_props[short_name]
        except KeyError:
            pass
        xml_add_propstat_to(response, short_name, 200)
    radicale_item.check_and_sanitize_props(new_props)
    collection.set_meta(new_props)

    return multistatus
Exemple #5
0
def prop_match(vobject_item, filter_, ns):
    """Check whether the ``item`` matches the prop ``filter_``.

    See rfc4791-9.7.2 and rfc6352-10.5.1.

    """
    name = filter_.get("name").lower()
    if len(filter_) == 0:
        # Point #1 of rfc4791-9.7.2
        return name in vobject_item.contents
    if len(filter_) == 1:
        if filter_[0].tag == xmlutils.make_tag("C", "is-not-defined"):
            # Point #2 of rfc4791-9.7.2
            return name not in vobject_item.contents
    if name not in vobject_item.contents:
        return False
    # Point #3 and #4 of rfc4791-9.7.2
    for child in filter_:
        if ns == "C" and child.tag == xmlutils.make_tag("C", "time-range"):
            if not time_range_match(vobject_item, child, name):
                return False
        elif child.tag == xmlutils.make_tag(ns, "text-match"):
            if not text_match(vobject_item, child, name, ns):
                return False
        elif child.tag == xmlutils.make_tag(ns, "param-filter"):
            if not param_filter_match(vobject_item, child, name, ns):
                return False
        else:
            raise ValueError("Unexpected %r in prop-filter" % child.tag)
    return True
Exemple #6
0
def xml_proppatch(base_prefix, path, xml_request, collection):
    """Read and answer PROPPATCH requests.

    Read rfc4918-9.2 for info.

    """
    props_to_set = xmlutils.props_from_request(xml_request, actions=("set", ))
    props_to_remove = xmlutils.props_from_request(xml_request,
                                                  actions=("remove", ))

    multistatus = ET.Element(xmlutils.make_tag("D", "multistatus"))
    response = ET.Element(xmlutils.make_tag("D", "response"))
    multistatus.append(response)

    href = ET.Element(xmlutils.make_tag("D", "href"))
    href.text = xmlutils.make_href(base_prefix, path)
    response.append(href)

    new_props = collection.get_meta()
    for short_name, value in props_to_set.items():
        new_props[short_name] = value
        xml_add_propstat_to(response, short_name, 200)
    for short_name in props_to_remove:
        try:
            del new_props[short_name]
        except KeyError:
            pass
        xml_add_propstat_to(response, short_name, 200)
    radicale_item.check_and_sanitize_props(new_props)
    collection.set_meta(new_props)

    return multistatus
Exemple #7
0
def comp_match(item, filter_, level=0):
    """Check whether the ``item`` matches the comp ``filter_``.

    If ``level`` is ``0``, the filter is applied on the
    item's collection. Otherwise, it's applied on the item.

    See rfc4791-9.7.1.

    """

    # TODO: Filtering VALARM and VFREEBUSY is not implemented
    # HACK: the filters are tested separately against all components

    if level == 0:
        tag = item.name
    elif level == 1:
        tag = item.component_name
    else:
        logger.warning(
            "Filters with three levels of comp-filter are not supported")
        return True
    if not tag:
        return False
    name = filter_.get("name").upper()
    if len(filter_) == 0:
        # Point #1 of rfc4791-9.7.1
        return name == tag
    if len(filter_) == 1:
        if filter_[0].tag == xmlutils.make_tag("C", "is-not-defined"):
            # Point #2 of rfc4791-9.7.1
            return name != tag
    if name != tag:
        return False
    if (level == 0 and name != "VCALENDAR" or
            level == 1 and name not in ("VTODO", "VEVENT", "VJOURNAL")):
        logger.warning("Filtering %s is not supported" % name)
        return True
    # Point #3 and #4 of rfc4791-9.7.1
    components = ([item.vobject_item] if level == 0
                  else list(getattr(item.vobject_item,
                                    "%s_list" % tag.lower())))
    for child in filter_:
        if child.tag == xmlutils.make_tag("C", "prop-filter"):
            if not any(prop_match(comp, child, "C")
                       for comp in components):
                return False
        elif child.tag == xmlutils.make_tag("C", "time-range"):
            if not time_range_match(item.vobject_item, filter_[0], tag):
                return False
        elif child.tag == xmlutils.make_tag("C", "comp-filter"):
            if not comp_match(item, child, level=level + 1):
                return False
        else:
            raise ValueError("Unexpected %r in comp-filter" % child.tag)
    return True
Exemple #8
0
def comp_match(item, filter_, level=0):
    """Check whether the ``item`` matches the comp ``filter_``.

    If ``level`` is ``0``, the filter is applied on the
    item's collection. Otherwise, it's applied on the item.

    See rfc4791-9.7.1.

    """

    # TODO: Filtering VALARM and VFREEBUSY is not implemented
    # HACK: the filters are tested separately against all components

    if level == 0:
        tag = item.name
    elif level == 1:
        tag = item.component_name
    else:
        logger.warning(
            "Filters with three levels of comp-filter are not supported")
        return True
    if not tag:
        return False
    name = filter_.get("name").upper()
    if len(filter_) == 0:
        # Point #1 of rfc4791-9.7.1
        return name == tag
    if len(filter_) == 1:
        if filter_[0].tag == xmlutils.make_tag("C", "is-not-defined"):
            # Point #2 of rfc4791-9.7.1
            return name != tag
    if name != tag:
        return False
    if (level == 0 and name != "VCALENDAR" or
            level == 1 and name not in ("VTODO", "VEVENT", "VJOURNAL")):
        logger.warning("Filtering %s is not supported" % name)
        return True
    # Point #3 and #4 of rfc4791-9.7.1
    components = ([item.vobject_item] if level == 0
                  else list(getattr(item.vobject_item,
                                    "%s_list" % tag.lower())))
    for child in filter_:
        if child.tag == xmlutils.make_tag("C", "prop-filter"):
            if not any(prop_match(comp, child, "C")
                       for comp in components):
                return False
        elif child.tag == xmlutils.make_tag("C", "time-range"):
            if not time_range_match(item.vobject_item, filter_[0], tag):
                return False
        elif child.tag == xmlutils.make_tag("C", "comp-filter"):
            if not comp_match(item, child, level=level + 1):
                return False
        else:
            raise ValueError("Unexpected %r in comp-filter" % child.tag)
    return True
Exemple #9
0
def simplify_prefilters(filters, collection_tag="VCALENDAR"):
    """Creates a simplified condition from ``filters``.

    Returns a tuple (``tag``, ``start``, ``end``, ``simple``) where ``tag`` is
    a string or None (match all) and ``start`` and ``end`` are POSIX
    timestamps (as int). ``simple`` is a bool that indicates that ``filters``
    and the simplified condition are identical.

    """
    flat_filters = tuple(chain.from_iterable(filters))
    simple = len(flat_filters) <= 1
    for col_filter in flat_filters:
        if collection_tag != "VCALENDAR":
            simple = False
            break
        if (col_filter.tag != xmlutils.make_tag("C", "comp-filter") or
                col_filter.get("name").upper() != "VCALENDAR"):
            simple = False
            continue
        simple &= len(col_filter) <= 1
        for comp_filter in col_filter:
            if comp_filter.tag != xmlutils.make_tag("C", "comp-filter"):
                simple = False
                continue
            tag = comp_filter.get("name").upper()
            if comp_filter.find(
                    xmlutils.make_tag("C", "is-not-defined")) is not None:
                simple = False
                continue
            simple &= len(comp_filter) <= 1
            for time_filter in comp_filter:
                if tag not in ("VTODO", "VEVENT", "VJOURNAL"):
                    simple = False
                    break
                if time_filter.tag != xmlutils.make_tag("C", "time-range"):
                    simple = False
                    continue
                start = time_filter.get("start")
                end = time_filter.get("end")
                if start:
                    start = math.floor(datetime.strptime(
                        start, "%Y%m%dT%H%M%SZ").replace(
                            tzinfo=timezone.utc).timestamp())
                else:
                    start = TIMESTAMP_MIN
                if end:
                    end = math.ceil(datetime.strptime(
                        end, "%Y%m%dT%H%M%SZ").replace(
                            tzinfo=timezone.utc).timestamp())
                else:
                    end = TIMESTAMP_MAX
                return tag, start, end, simple
            return tag, TIMESTAMP_MIN, TIMESTAMP_MAX, simple
    return None, TIMESTAMP_MIN, TIMESTAMP_MAX, simple
Exemple #10
0
def simplify_prefilters(filters, collection_tag="VCALENDAR"):
    """Creates a simplified condition from ``filters``.

    Returns a tuple (``tag``, ``start``, ``end``, ``simple``) where ``tag`` is
    a string or None (match all) and ``start`` and ``end`` are POSIX
    timestamps (as int). ``simple`` is a bool that indicates that ``filters``
    and the simplified condition are identical.

    """
    flat_filters = tuple(chain.from_iterable(filters))
    simple = len(flat_filters) <= 1
    for col_filter in flat_filters:
        if collection_tag != "VCALENDAR":
            simple = False
            break
        if (col_filter.tag != xmlutils.make_tag("C", "comp-filter")
                or col_filter.get("name").upper() != "VCALENDAR"):
            simple = False
            continue
        simple &= len(col_filter) <= 1
        for comp_filter in col_filter:
            if comp_filter.tag != xmlutils.make_tag("C", "comp-filter"):
                simple = False
                continue
            tag = comp_filter.get("name").upper()
            if comp_filter.find(xmlutils.make_tag(
                    "C", "is-not-defined")) is not None:
                simple = False
                continue
            simple &= len(comp_filter) <= 1
            for time_filter in comp_filter:
                if tag not in ("VTODO", "VEVENT", "VJOURNAL"):
                    simple = False
                    break
                if time_filter.tag != xmlutils.make_tag("C", "time-range"):
                    simple = False
                    continue
                start = time_filter.get("start")
                end = time_filter.get("end")
                if start:
                    start = math.floor(
                        datetime.strptime(start, "%Y%m%dT%H%M%SZ").replace(
                            tzinfo=timezone.utc).timestamp())
                else:
                    start = TIMESTAMP_MIN
                if end:
                    end = math.ceil(
                        datetime.strptime(end, "%Y%m%dT%H%M%SZ").replace(
                            tzinfo=timezone.utc).timestamp())
                else:
                    end = TIMESTAMP_MAX
                return tag, start, end, simple
            return tag, TIMESTAMP_MIN, TIMESTAMP_MAX, simple
    return None, TIMESTAMP_MIN, TIMESTAMP_MAX, simple
Exemple #11
0
def param_filter_match(vobject_item, filter_, parent_name, ns):
    """Check whether the ``item`` matches the param-filter ``filter_``.

    See rfc4791-9.7.3.

    """
    name = filter_.get("name").upper()
    children = getattr(vobject_item, "%s_list" % parent_name, [])
    condition = any(name in child.params for child in children)
    if len(filter_):
        if filter_[0].tag == xmlutils.make_tag(ns, "text-match"):
            return condition and text_match(
                vobject_item, filter_[0], parent_name, ns, name)
        elif filter_[0].tag == xmlutils.make_tag(ns, "is-not-defined"):
            return not condition
    else:
        return condition
Exemple #12
0
def param_filter_match(vobject_item, filter_, parent_name, ns):
    """Check whether the ``item`` matches the param-filter ``filter_``.

    See rfc4791-9.7.3.

    """
    name = filter_.get("name").upper()
    children = getattr(vobject_item, "%s_list" % parent_name, [])
    condition = any(name in child.params for child in children)
    if len(filter_):
        if filter_[0].tag == xmlutils.make_tag(ns, "text-match"):
            return condition and text_match(vobject_item, filter_[0],
                                            parent_name, ns, name)
        elif filter_[0].tag == xmlutils.make_tag(ns, "is-not-defined"):
            return not condition
    else:
        return condition
Exemple #13
0
def xml_add_propstat_to(element, tag, status_number):
    """Add a PROPSTAT response structure to an element.

    The PROPSTAT answer structure is defined in rfc4918-9.1. It is added to the
    given ``element``, for the following ``tag`` with the given
    ``status_number``.

    """
    propstat = ET.Element(xmlutils.make_tag("D", "propstat"))
    element.append(propstat)

    prop = ET.Element(xmlutils.make_tag("D", "prop"))
    propstat.append(prop)

    clark_tag = tag if "{" in tag else xmlutils.make_tag(*tag.split(":", 1))
    prop_tag = ET.Element(clark_tag)
    prop.append(prop_tag)

    status = ET.Element(xmlutils.make_tag("D", "status"))
    status.text = xmlutils.make_response(status_number)
    propstat.append(status)
Exemple #14
0
def xml_delete(base_prefix, path, collection, href=None):
    """Read and answer DELETE requests.

    Read rfc4918-9.6 for info.

    """
    collection.delete(href)

    multistatus = ET.Element(xmlutils.make_tag("D", "multistatus"))
    response = ET.Element(xmlutils.make_tag("D", "response"))
    multistatus.append(response)

    href = ET.Element(xmlutils.make_tag("D", "href"))
    href.text = xmlutils.make_href(base_prefix, path)
    response.append(href)

    status = ET.Element(xmlutils.make_tag("D", "status"))
    status.text = xmlutils.make_response(200)
    response.append(status)

    return multistatus
Exemple #15
0
def xml_delete(base_prefix, path, collection, href=None):
    """Read and answer DELETE requests.

    Read rfc4918-9.6 for info.

    """
    collection.delete(href)

    multistatus = ET.Element(xmlutils.make_tag("D", "multistatus"))
    response = ET.Element(xmlutils.make_tag("D", "response"))
    multistatus.append(response)

    href = ET.Element(xmlutils.make_tag("D", "href"))
    href.text = xmlutils.make_href(base_prefix, path)
    response.append(href)

    status = ET.Element(xmlutils.make_tag("D", "status"))
    status.text = xmlutils.make_response(200)
    response.append(status)

    return multistatus
Exemple #16
0
def xml_add_propstat_to(element, tag, status_number):
    """Add a PROPSTAT response structure to an element.

    The PROPSTAT answer structure is defined in rfc4918-9.1. It is added to the
    given ``element``, for the following ``tag`` with the given
    ``status_number``.

    """
    propstat = ET.Element(xmlutils.make_tag("D", "propstat"))
    element.append(propstat)

    prop = ET.Element(xmlutils.make_tag("D", "prop"))
    propstat.append(prop)

    clark_tag = tag if "{" in tag else xmlutils.make_tag(*tag.split(":", 1))
    prop_tag = ET.Element(clark_tag)
    prop.append(prop_tag)

    status = ET.Element(xmlutils.make_tag("D", "status"))
    status.text = xmlutils.make_response(status_number)
    propstat.append(status)
Exemple #17
0
def xml_item_response(base_prefix, href, found_props=(), not_found_props=(),
                      found_item=True):
    response = ET.Element(xmlutils.make_tag("D", "response"))

    href_tag = ET.Element(xmlutils.make_tag("D", "href"))
    href_tag.text = xmlutils.make_href(base_prefix, href)
    response.append(href_tag)

    if found_item:
        for code, props in ((200, found_props), (404, not_found_props)):
            if props:
                propstat = ET.Element(xmlutils.make_tag("D", "propstat"))
                status = ET.Element(xmlutils.make_tag("D", "status"))
                status.text = xmlutils.make_response(code)
                prop_tag = ET.Element(xmlutils.make_tag("D", "prop"))
                for prop in props:
                    prop_tag.append(prop)
                propstat.append(prop_tag)
                propstat.append(status)
                response.append(propstat)
    else:
        status = ET.Element(xmlutils.make_tag("D", "status"))
        status.text = xmlutils.make_response(404)
        response.append(status)

    return response
Exemple #18
0
def xml_item_response(base_prefix, href, found_props=(), not_found_props=(),
                      found_item=True):
    response = ET.Element(xmlutils.make_tag("D", "response"))

    href_tag = ET.Element(xmlutils.make_tag("D", "href"))
    href_tag.text = xmlutils.make_href(base_prefix, href)
    response.append(href_tag)

    if found_item:
        for code, props in ((200, found_props), (404, not_found_props)):
            if props:
                propstat = ET.Element(xmlutils.make_tag("D", "propstat"))
                status = ET.Element(xmlutils.make_tag("D", "status"))
                status.text = xmlutils.make_response(code)
                prop_tag = ET.Element(xmlutils.make_tag("D", "prop"))
                for prop in props:
                    prop_tag.append(prop)
                propstat.append(prop_tag)
                propstat.append(status)
                response.append(propstat)
    else:
        status = ET.Element(xmlutils.make_tag("D", "status"))
        status.text = xmlutils.make_response(404)
        response.append(status)

    return response
Exemple #19
0
def xml_propfind(base_prefix, path, xml_request, allowed_items, user):
    """Read and answer PROPFIND requests.

    Read rfc4918-9.1 for info.

    The collections parameter is a list of collections that are to be included
    in the output.

    """
    # A client may choose not to submit a request body.  An empty PROPFIND
    # request body MUST be treated as if it were an 'allprop' request.
    top_tag = (xml_request[0] if xml_request is not None else ET.Element(
        xmlutils.make_tag("D", "allprop")))

    props = ()
    allprop = False
    propname = False
    if top_tag.tag == xmlutils.make_tag("D", "allprop"):
        allprop = True
    elif top_tag.tag == xmlutils.make_tag("D", "propname"):
        propname = True
    elif top_tag.tag == xmlutils.make_tag("D", "prop"):
        props = [prop.tag for prop in top_tag]

    if xmlutils.make_tag("D", "current-user-principal") in props and not user:
        # Ask for authentication
        # Returning the DAV:unauthenticated pseudo-principal as specified in
        # RFC 5397 doesn't seem to work with DAVdroid.
        return client.FORBIDDEN, None

    # Writing answer
    multistatus = ET.Element(xmlutils.make_tag("D", "multistatus"))

    for item, permission in allowed_items:
        write = permission == "w"
        response = xml_propfind_response(base_prefix,
                                         path,
                                         item,
                                         props,
                                         user,
                                         write=write,
                                         allprop=allprop,
                                         propname=propname)
        if response:
            multistatus.append(response)

    return client.MULTI_STATUS, multistatus
Exemple #20
0
def xml_propfind(base_prefix, path, xml_request, allowed_items, user):
    """Read and answer PROPFIND requests.

    Read rfc4918-9.1 for info.

    The collections parameter is a list of collections that are to be included
    in the output.

    """
    # A client may choose not to submit a request body.  An empty PROPFIND
    # request body MUST be treated as if it were an 'allprop' request.
    top_tag = (xml_request[0] if xml_request is not None else
               ET.Element(xmlutils.make_tag("D", "allprop")))

    props = ()
    allprop = False
    propname = False
    if top_tag.tag == xmlutils.make_tag("D", "allprop"):
        allprop = True
    elif top_tag.tag == xmlutils.make_tag("D", "propname"):
        propname = True
    elif top_tag.tag == xmlutils.make_tag("D", "prop"):
        props = [prop.tag for prop in top_tag]

    if xmlutils.make_tag("D", "current-user-principal") in props and not user:
        # Ask for authentication
        # Returning the DAV:unauthenticated pseudo-principal as specified in
        # RFC 5397 doesn't seem to work with DAVdroid.
        return client.FORBIDDEN, None

    # Writing answer
    multistatus = ET.Element(xmlutils.make_tag("D", "multistatus"))

    for item, permission in allowed_items:
        write = permission == "w"
        response = xml_propfind_response(
            base_prefix, path, item, props, user, write=write,
            allprop=allprop, propname=propname)
        if response:
            multistatus.append(response)

    return client.MULTI_STATUS, multistatus
Exemple #21
0
def xml_propfind_response(base_prefix, path, item, props, user, write=False,
                          propname=False, allprop=False):
    """Build and return a PROPFIND response."""
    if propname and allprop or (props and (propname or allprop)):
        raise ValueError("Only use one of props, propname and allprops")
    is_collection = isinstance(item, storage.BaseCollection)
    if is_collection:
        is_leaf = item.get_meta("tag") in ("VADDRESSBOOK", "VCALENDAR")
        collection = item
    else:
        collection = item.collection

    response = ET.Element(xmlutils.make_tag("D", "response"))

    href = ET.Element(xmlutils.make_tag("D", "href"))
    if is_collection:
        # Some clients expect collections to end with /
        uri = pathutils.unstrip_path(item.path, True)
    else:
        uri = pathutils.unstrip_path(
            posixpath.join(collection.path, item.href))

    href.text = xmlutils.make_href(base_prefix, uri)
    response.append(href)

    propstat404 = ET.Element(xmlutils.make_tag("D", "propstat"))
    propstat200 = ET.Element(xmlutils.make_tag("D", "propstat"))
    response.append(propstat200)

    prop200 = ET.Element(xmlutils.make_tag("D", "prop"))
    propstat200.append(prop200)

    prop404 = ET.Element(xmlutils.make_tag("D", "prop"))
    propstat404.append(prop404)

    if propname or allprop:
        props = []
        # Should list all properties that can be retrieved by the code below
        props.append(xmlutils.make_tag("D", "principal-collection-set"))
        props.append(xmlutils.make_tag("D", "current-user-principal"))
        props.append(xmlutils.make_tag("D", "current-user-privilege-set"))
        props.append(xmlutils.make_tag("D", "supported-report-set"))
        props.append(xmlutils.make_tag("D", "resourcetype"))
        props.append(xmlutils.make_tag("D", "owner"))

        if is_collection and collection.is_principal:
            props.append(xmlutils.make_tag("C", "calendar-user-address-set"))
            props.append(xmlutils.make_tag("D", "principal-URL"))
            props.append(xmlutils.make_tag("CR", "addressbook-home-set"))
            props.append(xmlutils.make_tag("C", "calendar-home-set"))

        if not is_collection or is_leaf:
            props.append(xmlutils.make_tag("D", "getetag"))
            props.append(xmlutils.make_tag("D", "getlastmodified"))
            props.append(xmlutils.make_tag("D", "getcontenttype"))
            props.append(xmlutils.make_tag("D", "getcontentlength"))

        if is_collection:
            if is_leaf:
                props.append(xmlutils.make_tag("D", "displayname"))
                props.append(xmlutils.make_tag("D", "sync-token"))
            if collection.get_meta("tag") == "VCALENDAR":
                props.append(xmlutils.make_tag("CS", "getctag"))
                props.append(
                    xmlutils.make_tag("C", "supported-calendar-component-set"))

            meta = item.get_meta()
            for tag in meta:
                if tag == "tag":
                    continue
                clark_tag = xmlutils.tag_from_human(tag)
                if clark_tag not in props:
                    props.append(clark_tag)

    if propname:
        for tag in props:
            prop200.append(ET.Element(tag))
        props = ()

    for tag in props:
        element = ET.Element(tag)
        is404 = False
        if tag == xmlutils.make_tag("D", "getetag"):
            if not is_collection or is_leaf:
                element.text = item.etag
            else:
                is404 = True
        elif tag == xmlutils.make_tag("D", "getlastmodified"):
            if not is_collection or is_leaf:
                element.text = item.last_modified
            else:
                is404 = True
        elif tag == xmlutils.make_tag("D", "principal-collection-set"):
            tag = ET.Element(xmlutils.make_tag("D", "href"))
            tag.text = xmlutils.make_href(base_prefix, "/")
            element.append(tag)
        elif (tag in (xmlutils.make_tag("C", "calendar-user-address-set"),
                      xmlutils.make_tag("D", "principal-URL"),
                      xmlutils.make_tag("CR", "addressbook-home-set"),
                      xmlutils.make_tag("C", "calendar-home-set")) and
                collection.is_principal and is_collection):
            tag = ET.Element(xmlutils.make_tag("D", "href"))
            tag.text = xmlutils.make_href(base_prefix, path)
            element.append(tag)
        elif tag == xmlutils.make_tag("C", "supported-calendar-component-set"):
            human_tag = xmlutils.tag_from_clark(tag)
            if is_collection and is_leaf:
                meta = item.get_meta(human_tag)
                if meta:
                    components = meta.split(",")
                else:
                    components = ("VTODO", "VEVENT", "VJOURNAL")
                for component in components:
                    comp = ET.Element(xmlutils.make_tag("C", "comp"))
                    comp.set("name", component)
                    element.append(comp)
            else:
                is404 = True
        elif tag == xmlutils.make_tag("D", "current-user-principal"):
            if user:
                tag = ET.Element(xmlutils.make_tag("D", "href"))
                tag.text = xmlutils.make_href(base_prefix, "/%s/" % user)
                element.append(tag)
            else:
                element.append(ET.Element(
                    xmlutils.make_tag("D", "unauthenticated")))
        elif tag == xmlutils.make_tag("D", "current-user-privilege-set"):
            privileges = [("D", "read")]
            if write:
                privileges.append(("D", "all"))
                privileges.append(("D", "write"))
                privileges.append(("D", "write-properties"))
                privileges.append(("D", "write-content"))
            for ns, privilege_name in privileges:
                privilege = ET.Element(xmlutils.make_tag("D", "privilege"))
                privilege.append(ET.Element(
                    xmlutils.make_tag(ns, privilege_name)))
                element.append(privilege)
        elif tag == xmlutils.make_tag("D", "supported-report-set"):
            # These 3 reports are not implemented
            reports = [
                ("D", "expand-property"),
                ("D", "principal-search-property-set"),
                ("D", "principal-property-search")]
            if is_collection and is_leaf:
                reports.append(("D", "sync-collection"))
                if item.get_meta("tag") == "VADDRESSBOOK":
                    reports.append(("CR", "addressbook-multiget"))
                    reports.append(("CR", "addressbook-query"))
                elif item.get_meta("tag") == "VCALENDAR":
                    reports.append(("C", "calendar-multiget"))
                    reports.append(("C", "calendar-query"))
            for ns, report_name in reports:
                supported = ET.Element(
                    xmlutils.make_tag("D", "supported-report"))
                report_tag = ET.Element(xmlutils.make_tag("D", "report"))
                supported_report_tag = ET.Element(
                    xmlutils.make_tag(ns, report_name))
                report_tag.append(supported_report_tag)
                supported.append(report_tag)
                element.append(supported)
        elif tag == xmlutils.make_tag("D", "getcontentlength"):
            if not is_collection or is_leaf:
                encoding = collection.configuration.get("encoding", "request")
                element.text = str(len(item.serialize().encode(encoding)))
            else:
                is404 = True
        elif tag == xmlutils.make_tag("D", "owner"):
            # return empty elment, if no owner available (rfc3744-5.1)
            if collection.owner:
                tag = ET.Element(xmlutils.make_tag("D", "href"))
                tag.text = xmlutils.make_href(
                    base_prefix, "/%s/" % collection.owner)
                element.append(tag)
        elif is_collection:
            if tag == xmlutils.make_tag("D", "getcontenttype"):
                if is_leaf:
                    element.text = xmlutils.MIMETYPES[item.get_meta("tag")]
                else:
                    is404 = True
            elif tag == xmlutils.make_tag("D", "resourcetype"):
                if item.is_principal:
                    tag = ET.Element(xmlutils.make_tag("D", "principal"))
                    element.append(tag)
                if is_leaf:
                    if item.get_meta("tag") == "VADDRESSBOOK":
                        tag = ET.Element(
                            xmlutils.make_tag("CR", "addressbook"))
                        element.append(tag)
                    elif item.get_meta("tag") == "VCALENDAR":
                        tag = ET.Element(xmlutils.make_tag("C", "calendar"))
                        element.append(tag)
                tag = ET.Element(xmlutils.make_tag("D", "collection"))
                element.append(tag)
            elif tag == xmlutils.make_tag("RADICALE", "displayname"):
                # Only for internal use by the web interface
                displayname = item.get_meta("D:displayname")
                if displayname is not None:
                    element.text = displayname
                else:
                    is404 = True
            elif tag == xmlutils.make_tag("D", "displayname"):
                displayname = item.get_meta("D:displayname")
                if not displayname and is_leaf:
                    displayname = item.path
                if displayname is not None:
                    element.text = displayname
                else:
                    is404 = True
            elif tag == xmlutils.make_tag("CS", "getctag"):
                if is_leaf:
                    element.text = item.etag
                else:
                    is404 = True
            elif tag == xmlutils.make_tag("D", "sync-token"):
                if is_leaf:
                    element.text, _ = item.sync()
                else:
                    is404 = True
            else:
                human_tag = xmlutils.tag_from_clark(tag)
                meta = item.get_meta(human_tag)
                if meta is not None:
                    element.text = meta
                else:
                    is404 = True
        # Not for collections
        elif tag == xmlutils.make_tag("D", "getcontenttype"):
            element.text = xmlutils.get_content_type(item)
        elif tag == xmlutils.make_tag("D", "resourcetype"):
            # resourcetype must be returned empty for non-collection elements
            pass
        else:
            is404 = True

        if is404:
            prop404.append(element)
        else:
            prop200.append(element)

    status200 = ET.Element(xmlutils.make_tag("D", "status"))
    status200.text = xmlutils.make_response(200)
    propstat200.append(status200)

    status404 = ET.Element(xmlutils.make_tag("D", "status"))
    status404.text = xmlutils.make_response(404)
    propstat404.append(status404)
    if len(prop404):
        response.append(propstat404)

    return response
Exemple #22
0
def xml_propfind_response(base_prefix,
                          path,
                          item,
                          props,
                          user,
                          write=False,
                          propname=False,
                          allprop=False):
    """Build and return a PROPFIND response."""
    if propname and allprop or (props and (propname or allprop)):
        raise ValueError("Only use one of props, propname and allprops")
    is_collection = isinstance(item, storage.BaseCollection)
    if is_collection:
        is_leaf = item.get_meta("tag") in ("VADDRESSBOOK", "VCALENDAR")
        collection = item
    else:
        collection = item.collection

    response = ET.Element(xmlutils.make_tag("D", "response"))

    href = ET.Element(xmlutils.make_tag("D", "href"))
    if is_collection:
        # Some clients expect collections to end with /
        uri = pathutils.unstrip_path(item.path, True)
    else:
        uri = pathutils.unstrip_path(posixpath.join(collection.path,
                                                    item.href))

    href.text = xmlutils.make_href(base_prefix, uri)
    response.append(href)

    propstat404 = ET.Element(xmlutils.make_tag("D", "propstat"))
    propstat200 = ET.Element(xmlutils.make_tag("D", "propstat"))
    response.append(propstat200)

    prop200 = ET.Element(xmlutils.make_tag("D", "prop"))
    propstat200.append(prop200)

    prop404 = ET.Element(xmlutils.make_tag("D", "prop"))
    propstat404.append(prop404)

    if propname or allprop:
        props = []
        # Should list all properties that can be retrieved by the code below
        props.append(xmlutils.make_tag("D", "principal-collection-set"))
        props.append(xmlutils.make_tag("D", "current-user-principal"))
        props.append(xmlutils.make_tag("D", "current-user-privilege-set"))
        props.append(xmlutils.make_tag("D", "supported-report-set"))
        props.append(xmlutils.make_tag("D", "resourcetype"))
        props.append(xmlutils.make_tag("D", "owner"))

        if is_collection and collection.is_principal:
            props.append(xmlutils.make_tag("C", "calendar-user-address-set"))
            props.append(xmlutils.make_tag("D", "principal-URL"))
            props.append(xmlutils.make_tag("CR", "addressbook-home-set"))
            props.append(xmlutils.make_tag("C", "calendar-home-set"))

        if not is_collection or is_leaf:
            props.append(xmlutils.make_tag("D", "getetag"))
            props.append(xmlutils.make_tag("D", "getlastmodified"))
            props.append(xmlutils.make_tag("D", "getcontenttype"))
            props.append(xmlutils.make_tag("D", "getcontentlength"))

        if is_collection:
            if is_leaf:
                props.append(xmlutils.make_tag("D", "displayname"))
                props.append(xmlutils.make_tag("D", "sync-token"))
            if collection.get_meta("tag") == "VCALENDAR":
                props.append(xmlutils.make_tag("CS", "getctag"))
                props.append(
                    xmlutils.make_tag("C", "supported-calendar-component-set"))

            meta = item.get_meta()
            for tag in meta:
                if tag == "tag":
                    continue
                clark_tag = xmlutils.tag_from_human(tag)
                if clark_tag not in props:
                    props.append(clark_tag)

    if propname:
        for tag in props:
            prop200.append(ET.Element(tag))
        props = ()

    for tag in props:
        element = ET.Element(tag)
        is404 = False
        if tag == xmlutils.make_tag("D", "getetag"):
            if not is_collection or is_leaf:
                element.text = item.etag
            else:
                is404 = True
        elif tag == xmlutils.make_tag("D", "getlastmodified"):
            if not is_collection or is_leaf:
                element.text = item.last_modified
            else:
                is404 = True
        elif tag == xmlutils.make_tag("D", "principal-collection-set"):
            tag = ET.Element(xmlutils.make_tag("D", "href"))
            tag.text = xmlutils.make_href(base_prefix, "/")
            element.append(tag)
        elif (tag in (xmlutils.make_tag("C", "calendar-user-address-set"),
                      xmlutils.make_tag("D", "principal-URL"),
                      xmlutils.make_tag("CR", "addressbook-home-set"),
                      xmlutils.make_tag("C", "calendar-home-set"))
              and collection.is_principal and is_collection):
            tag = ET.Element(xmlutils.make_tag("D", "href"))
            tag.text = xmlutils.make_href(base_prefix, path)
            element.append(tag)
        elif tag == xmlutils.make_tag("C", "supported-calendar-component-set"):
            human_tag = xmlutils.tag_from_clark(tag)
            if is_collection and is_leaf:
                meta = item.get_meta(human_tag)
                if meta:
                    components = meta.split(",")
                else:
                    components = ("VTODO", "VEVENT", "VJOURNAL")
                for component in components:
                    comp = ET.Element(xmlutils.make_tag("C", "comp"))
                    comp.set("name", component)
                    element.append(comp)
            else:
                is404 = True
        elif tag == xmlutils.make_tag("D", "current-user-principal"):
            if user:
                tag = ET.Element(xmlutils.make_tag("D", "href"))
                tag.text = xmlutils.make_href(base_prefix, "/%s/" % user)
                element.append(tag)
            else:
                element.append(
                    ET.Element(xmlutils.make_tag("D", "unauthenticated")))
        elif tag == xmlutils.make_tag("D", "current-user-privilege-set"):
            privileges = [("D", "read")]
            if write:
                privileges.append(("D", "all"))
                privileges.append(("D", "write"))
                privileges.append(("D", "write-properties"))
                privileges.append(("D", "write-content"))
            for ns, privilege_name in privileges:
                privilege = ET.Element(xmlutils.make_tag("D", "privilege"))
                privilege.append(
                    ET.Element(xmlutils.make_tag(ns, privilege_name)))
                element.append(privilege)
        elif tag == xmlutils.make_tag("D", "supported-report-set"):
            # These 3 reports are not implemented
            reports = [("D", "expand-property"),
                       ("D", "principal-search-property-set"),
                       ("D", "principal-property-search")]
            if is_collection and is_leaf:
                reports.append(("D", "sync-collection"))
                if item.get_meta("tag") == "VADDRESSBOOK":
                    reports.append(("CR", "addressbook-multiget"))
                    reports.append(("CR", "addressbook-query"))
                elif item.get_meta("tag") == "VCALENDAR":
                    reports.append(("C", "calendar-multiget"))
                    reports.append(("C", "calendar-query"))
            for ns, report_name in reports:
                supported = ET.Element(
                    xmlutils.make_tag("D", "supported-report"))
                report_tag = ET.Element(xmlutils.make_tag("D", "report"))
                supported_report_tag = ET.Element(
                    xmlutils.make_tag(ns, report_name))
                report_tag.append(supported_report_tag)
                supported.append(report_tag)
                element.append(supported)
        elif tag == xmlutils.make_tag("D", "getcontentlength"):
            if not is_collection or is_leaf:
                encoding = collection.configuration.get("encoding", "request")
                element.text = str(len(item.serialize().encode(encoding)))
            else:
                is404 = True
        elif tag == xmlutils.make_tag("D", "owner"):
            # return empty elment, if no owner available (rfc3744-5.1)
            if collection.owner:
                tag = ET.Element(xmlutils.make_tag("D", "href"))
                tag.text = xmlutils.make_href(base_prefix,
                                              "/%s/" % collection.owner)
                element.append(tag)
        elif is_collection:
            if tag == xmlutils.make_tag("D", "getcontenttype"):
                if is_leaf:
                    element.text = xmlutils.MIMETYPES[item.get_meta("tag")]
                else:
                    is404 = True
            elif tag == xmlutils.make_tag("D", "resourcetype"):
                if item.is_principal:
                    tag = ET.Element(xmlutils.make_tag("D", "principal"))
                    element.append(tag)
                if is_leaf:
                    if item.get_meta("tag") == "VADDRESSBOOK":
                        tag = ET.Element(xmlutils.make_tag(
                            "CR", "addressbook"))
                        element.append(tag)
                    elif item.get_meta("tag") == "VCALENDAR":
                        tag = ET.Element(xmlutils.make_tag("C", "calendar"))
                        element.append(tag)
                tag = ET.Element(xmlutils.make_tag("D", "collection"))
                element.append(tag)
            elif tag == xmlutils.make_tag("RADICALE", "displayname"):
                # Only for internal use by the web interface
                displayname = item.get_meta("D:displayname")
                if displayname is not None:
                    element.text = displayname
                else:
                    is404 = True
            elif tag == xmlutils.make_tag("D", "displayname"):
                displayname = item.get_meta("D:displayname")
                if not displayname and is_leaf:
                    displayname = item.path
                if displayname is not None:
                    element.text = displayname
                else:
                    is404 = True
            elif tag == xmlutils.make_tag("CS", "getctag"):
                if is_leaf:
                    element.text = item.etag
                else:
                    is404 = True
            elif tag == xmlutils.make_tag("D", "sync-token"):
                if is_leaf:
                    element.text, _ = item.sync()
                else:
                    is404 = True
            else:
                human_tag = xmlutils.tag_from_clark(tag)
                meta = item.get_meta(human_tag)
                if meta is not None:
                    element.text = meta
                else:
                    is404 = True
        # Not for collections
        elif tag == xmlutils.make_tag("D", "getcontenttype"):
            element.text = xmlutils.get_content_type(item)
        elif tag == xmlutils.make_tag("D", "resourcetype"):
            # resourcetype must be returned empty for non-collection elements
            pass
        else:
            is404 = True

        if is404:
            prop404.append(element)
        else:
            prop200.append(element)

    status200 = ET.Element(xmlutils.make_tag("D", "status"))
    status200.text = xmlutils.make_response(200)
    propstat200.append(status200)

    status404 = ET.Element(xmlutils.make_tag("D", "status"))
    status404.text = xmlutils.make_response(404)
    propstat404.append(status404)
    if len(prop404):
        response.append(propstat404)

    return response
Exemple #23
0
def xml_report(base_prefix, path, xml_request, collection, unlock_storage_fn):
    """Read and answer REPORT requests.

    Read rfc3253-3.6 for info.

    """
    multistatus = ET.Element(xmlutils.make_tag("D", "multistatus"))
    if xml_request is None:
        return client.MULTI_STATUS, multistatus
    root = xml_request
    if root.tag in (
            xmlutils.make_tag("D", "principal-search-property-set"),
            xmlutils.make_tag("D", "principal-property-search"),
            xmlutils.make_tag("D", "expand-property")):
        # We don't support searching for principals or indirect retrieving of
        # properties, just return an empty result.
        # InfCloud asks for expand-property reports (even if we don't announce
        # support for them) and stops working if an error code is returned.
        logger.warning("Unsupported REPORT method %r on %r requested",
                       xmlutils.tag_from_clark(root.tag), path)
        return client.MULTI_STATUS, multistatus
    if (root.tag == xmlutils.make_tag("C", "calendar-multiget") and
            collection.get_meta("tag") != "VCALENDAR" or
            root.tag == xmlutils.make_tag("CR", "addressbook-multiget") and
            collection.get_meta("tag") != "VADDRESSBOOK" or
            root.tag == xmlutils.make_tag("D", "sync-collection") and
            collection.get_meta("tag") not in ("VADDRESSBOOK", "VCALENDAR")):
        logger.warning("Invalid REPORT method %r on %r requested",
                       xmlutils.tag_from_clark(root.tag), path)
        return (client.CONFLICT,
                xmlutils.webdav_error("D", "supported-report"))
    prop_element = root.find(xmlutils.make_tag("D", "prop"))
    props = (
        [prop.tag for prop in prop_element]
        if prop_element is not None else [])

    if root.tag in (
            xmlutils.make_tag("C", "calendar-multiget"),
            xmlutils.make_tag("CR", "addressbook-multiget")):
        # Read rfc4791-7.9 for info
        hreferences = set()
        for href_element in root.findall(xmlutils.make_tag("D", "href")):
            href_path = pathutils.sanitize_path(
                unquote(urlparse(href_element.text).path))
            if (href_path + "/").startswith(base_prefix + "/"):
                hreferences.add(href_path[len(base_prefix):])
            else:
                logger.warning("Skipping invalid path %r in REPORT request on "
                               "%r", href_path, path)
    elif root.tag == xmlutils.make_tag("D", "sync-collection"):
        old_sync_token_element = root.find(
            xmlutils.make_tag("D", "sync-token"))
        old_sync_token = ""
        if old_sync_token_element is not None and old_sync_token_element.text:
            old_sync_token = old_sync_token_element.text.strip()
        logger.debug("Client provided sync token: %r", old_sync_token)
        try:
            sync_token, names = collection.sync(old_sync_token)
        except ValueError as e:
            # Invalid sync token
            logger.warning("Client provided invalid sync token %r: %s",
                           old_sync_token, e, exc_info=True)
            return (client.CONFLICT,
                    xmlutils.webdav_error("D", "valid-sync-token"))
        hreferences = (pathutils.unstrip_path(
            posixpath.join(collection.path, n)) for n in names)
        # Append current sync token to response
        sync_token_element = ET.Element(xmlutils.make_tag("D", "sync-token"))
        sync_token_element.text = sync_token
        multistatus.append(sync_token_element)
    else:
        hreferences = (path,)
    filters = (
        root.findall("./%s" % xmlutils.make_tag("C", "filter")) +
        root.findall("./%s" % xmlutils.make_tag("CR", "filter")))

    def retrieve_items(collection, hreferences, multistatus):
        """Retrieves all items that are referenced in ``hreferences`` from
           ``collection`` and adds 404 responses for missing and invalid items
           to ``multistatus``."""
        collection_requested = False

        def get_names():
            """Extracts all names from references in ``hreferences`` and adds
               404 responses for invalid references to ``multistatus``.
               If the whole collections is referenced ``collection_requested``
               gets set to ``True``."""
            nonlocal collection_requested
            for hreference in hreferences:
                try:
                    name = pathutils.name_from_path(hreference, collection)
                except ValueError as e:
                    logger.warning("Skipping invalid path %r in REPORT request"
                                   " on %r: %s", hreference, path, e)
                    response = xml_item_response(base_prefix, hreference,
                                                 found_item=False)
                    multistatus.append(response)
                    continue
                if name:
                    # Reference is an item
                    yield name
                else:
                    # Reference is a collection
                    collection_requested = True

        for name, item in collection.get_multi(get_names()):
            if not item:
                uri = pathutils.unstrip_path(
                    posixpath.join(collection.path, name))
                response = xml_item_response(base_prefix, uri,
                                             found_item=False)
                multistatus.append(response)
            else:
                yield item, False
        if collection_requested:
            yield from collection.get_filtered(filters)

    # Retrieve everything required for finishing the request.
    retrieved_items = list(retrieve_items(collection, hreferences,
                                          multistatus))
    collection_tag = collection.get_meta("tag")
    # Don't access storage after this!
    unlock_storage_fn()

    def match(item, filter_):
        tag = collection_tag
        if (tag == "VCALENDAR" and
                filter_.tag != xmlutils.make_tag("C", filter_)):
            if len(filter_) == 0:
                return True
            if len(filter_) > 1:
                raise ValueError("Filter with %d children" % len(filter_))
            if filter_[0].tag != xmlutils.make_tag("C", "comp-filter"):
                raise ValueError("Unexpected %r in filter" % filter_[0].tag)
            return radicale_filter.comp_match(item, filter_[0])
        if (tag == "VADDRESSBOOK" and
                filter_.tag != xmlutils.make_tag("CR", filter_)):
            for child in filter_:
                if child.tag != xmlutils.make_tag("CR", "prop-filter"):
                    raise ValueError("Unexpected %r in filter" % child.tag)
            test = filter_.get("test", "anyof")
            if test == "anyof":
                return any(
                    radicale_filter.prop_match(item.vobject_item, f, "CR")
                    for f in filter_)
            if test == "allof":
                return all(
                    radicale_filter.prop_match(item.vobject_item, f, "CR")
                    for f in filter_)
            raise ValueError("Unsupported filter test: %r" % test)
            return all(radicale_filter.prop_match(item.vobject_item, f, "CR")
                       for f in filter_)
        raise ValueError("unsupported filter %r for %r" % (filter_.tag, tag))

    while retrieved_items:
        # ``item.vobject_item`` might be accessed during filtering.
        # Don't keep reference to ``item``, because VObject requires a lot of
        # memory.
        item, filters_matched = retrieved_items.pop(0)
        if filters and not filters_matched:
            try:
                if not all(match(item, filter_) for filter_ in filters):
                    continue
            except ValueError as e:
                raise ValueError("Failed to filter item %r from %r: %s" %
                                 (item.href, collection.path, e)) from e
            except Exception as e:
                raise RuntimeError("Failed to filter item %r from %r: %s" %
                                   (item.href, collection.path, e)) from e

        found_props = []
        not_found_props = []

        for tag in props:
            element = ET.Element(tag)
            if tag == xmlutils.make_tag("D", "getetag"):
                element.text = item.etag
                found_props.append(element)
            elif tag == xmlutils.make_tag("D", "getcontenttype"):
                element.text = xmlutils.get_content_type(item)
                found_props.append(element)
            elif tag in (
                    xmlutils.make_tag("C", "calendar-data"),
                    xmlutils.make_tag("CR", "address-data")):
                element.text = item.serialize()
                found_props.append(element)
            else:
                not_found_props.append(element)

        uri = pathutils.unstrip_path(
            posixpath.join(collection.path, item.href))
        multistatus.append(xml_item_response(
            base_prefix, uri, found_props=found_props,
            not_found_props=not_found_props, found_item=True))

    return client.MULTI_STATUS, multistatus
Exemple #24
0
def xml_report(base_prefix, path, xml_request, collection, unlock_storage_fn):
    """Read and answer REPORT requests.

    Read rfc3253-3.6 for info.

    """
    multistatus = ET.Element(xmlutils.make_tag("D", "multistatus"))
    if xml_request is None:
        return client.MULTI_STATUS, multistatus
    root = xml_request
    if root.tag in (
            xmlutils.make_tag("D", "principal-search-property-set"),
            xmlutils.make_tag("D", "principal-property-search"),
            xmlutils.make_tag("D", "expand-property")):
        # We don't support searching for principals or indirect retrieving of
        # properties, just return an empty result.
        # InfCloud asks for expand-property reports (even if we don't announce
        # support for them) and stops working if an error code is returned.
        logger.warning("Unsupported REPORT method %r on %r requested",
                       xmlutils.tag_from_clark(root.tag), path)
        return client.MULTI_STATUS, multistatus
    if (root.tag == xmlutils.make_tag("C", "calendar-multiget") and
            collection.get_meta("tag") != "VCALENDAR" or
            root.tag == xmlutils.make_tag("CR", "addressbook-multiget") and
            collection.get_meta("tag") != "VADDRESSBOOK" or
            root.tag == xmlutils.make_tag("D", "sync-collection") and
            collection.get_meta("tag") not in ("VADDRESSBOOK", "VCALENDAR")):
        logger.warning("Invalid REPORT method %r on %r requested",
                       xmlutils.tag_from_clark(root.tag), path)
        return (client.CONFLICT,
                xmlutils.webdav_error("D", "supported-report"))
    prop_element = root.find(xmlutils.make_tag("D", "prop"))
    props = (
        [prop.tag for prop in prop_element]
        if prop_element is not None else [])

    if root.tag in (
            xmlutils.make_tag("C", "calendar-multiget"),
            xmlutils.make_tag("CR", "addressbook-multiget")):
        # Read rfc4791-7.9 for info
        hreferences = set()
        for href_element in root.findall(xmlutils.make_tag("D", "href")):
            href_path = pathutils.sanitize_path(
                unquote(urlparse(href_element.text).path))
            if (href_path + "/").startswith(base_prefix + "/"):
                hreferences.add(href_path[len(base_prefix):])
            else:
                logger.warning("Skipping invalid path %r in REPORT request on "
                               "%r", href_path, path)
    elif root.tag == xmlutils.make_tag("D", "sync-collection"):
        old_sync_token_element = root.find(
            xmlutils.make_tag("D", "sync-token"))
        old_sync_token = ""
        if old_sync_token_element is not None and old_sync_token_element.text:
            old_sync_token = old_sync_token_element.text.strip()
        logger.debug("Client provided sync token: %r", old_sync_token)
        try:
            sync_token, names = collection.sync(old_sync_token)
        except ValueError as e:
            # Invalid sync token
            logger.warning("Client provided invalid sync token %r: %s",
                           old_sync_token, e, exc_info=True)
            return (client.CONFLICT,
                    xmlutils.webdav_error("D", "valid-sync-token"))
        hreferences = (pathutils.unstrip_path(
            posixpath.join(collection.path, n)) for n in names)
        # Append current sync token to response
        sync_token_element = ET.Element(xmlutils.make_tag("D", "sync-token"))
        sync_token_element.text = sync_token
        multistatus.append(sync_token_element)
    else:
        hreferences = (path,)
    filters = (
        root.findall("./%s" % xmlutils.make_tag("C", "filter")) +
        root.findall("./%s" % xmlutils.make_tag("CR", "filter")))

    def retrieve_items(collection, hreferences, multistatus):
        """Retrieves all items that are referenced in ``hreferences`` from
           ``collection`` and adds 404 responses for missing and invalid items
           to ``multistatus``."""
        collection_requested = False

        def get_names():
            """Extracts all names from references in ``hreferences`` and adds
               404 responses for invalid references to ``multistatus``.
               If the whole collections is referenced ``collection_requested``
               gets set to ``True``."""
            nonlocal collection_requested
            for hreference in hreferences:
                try:
                    name = pathutils.name_from_path(hreference, collection)
                except ValueError as e:
                    logger.warning("Skipping invalid path %r in REPORT request"
                                   " on %r: %s", hreference, path, e)
                    response = xml_item_response(base_prefix, hreference,
                                                 found_item=False)
                    multistatus.append(response)
                    continue
                if name:
                    # Reference is an item
                    yield name
                else:
                    # Reference is a collection
                    collection_requested = True

        for name, item in collection.get_multi(get_names()):
            if not item:
                uri = pathutils.unstrip_path(
                    posixpath.join(collection.path, name))
                response = xml_item_response(base_prefix, uri,
                                             found_item=False)
                multistatus.append(response)
            else:
                yield item, False
        if collection_requested:
            yield from collection.get_filtered(filters)

    # Retrieve everything required for finishing the request.
    retrieved_items = list(retrieve_items(collection, hreferences,
                                          multistatus))
    collection_tag = collection.get_meta("tag")
    # Don't access storage after this!
    unlock_storage_fn()

    def match(item, filter_):
        tag = collection_tag
        if (tag == "VCALENDAR" and
                filter_.tag != xmlutils.make_tag("C", filter_)):
            if len(filter_) == 0:
                return True
            if len(filter_) > 1:
                raise ValueError("Filter with %d children" % len(filter_))
            if filter_[0].tag != xmlutils.make_tag("C", "comp-filter"):
                raise ValueError("Unexpected %r in filter" % filter_[0].tag)
            return radicale_filter.comp_match(item, filter_[0])
        if (tag == "VADDRESSBOOK" and
                filter_.tag != xmlutils.make_tag("CR", filter_)):
            for child in filter_:
                if child.tag != xmlutils.make_tag("CR", "prop-filter"):
                    raise ValueError("Unexpected %r in filter" % child.tag)
            test = filter_.get("test", "anyof")
            if test == "anyof":
                return any(
                    radicale_filter.prop_match(item.vobject_item, f, "CR")
                    for f in filter_)
            if test == "allof":
                return all(
                    radicale_filter.prop_match(item.vobject_item, f, "CR")
                    for f in filter_)
            raise ValueError("Unsupported filter test: %r" % test)
            return all(radicale_filter.prop_match(item.vobject_item, f, "CR")
                       for f in filter_)
        raise ValueError("unsupported filter %r for %r" % (filter_.tag, tag))

    while retrieved_items:
        # ``item.vobject_item`` might be accessed during filtering.
        # Don't keep reference to ``item``, because VObject requires a lot of
        # memory.
        item, filters_matched = retrieved_items.pop(0)
        if filters and not filters_matched:
            try:
                if not all(match(item, filter_) for filter_ in filters):
                    continue
            except ValueError as e:
                raise ValueError("Failed to filter item %r from %r: %s" %
                                 (item.href, collection.path, e)) from e
            except Exception as e:
                raise RuntimeError("Failed to filter item %r from %r: %s" %
                                   (item.href, collection.path, e)) from e

        found_props = []
        not_found_props = []

        for tag in props:
            element = ET.Element(tag)
            if tag == xmlutils.make_tag("D", "getetag"):
                element.text = item.etag
                found_props.append(element)
            elif tag == xmlutils.make_tag("D", "getcontenttype"):
                element.text = xmlutils.get_content_type(item)
                found_props.append(element)
            elif tag in (
                    xmlutils.make_tag("C", "calendar-data"),
                    xmlutils.make_tag("CR", "address-data")):
                element.text = item.serialize()
                found_props.append(element)
            else:
                not_found_props.append(element)

        uri = pathutils.unstrip_path(
            posixpath.join(collection.path, item.href))
        multistatus.append(xml_item_response(
            base_prefix, uri, found_props=found_props,
            not_found_props=not_found_props, found_item=True))

    return client.MULTI_STATUS, multistatus