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