mirror of
https://github.com/Kozea/Radicale.git
synced 2025-08-07 18:30:54 +00:00
Synced with origin
This commit is contained in:
parent
9b3bb2de2b
commit
cf81d1f9a7
94 changed files with 5096 additions and 3560 deletions
|
@ -1,4 +1,4 @@
|
|||
# This file is part of Radicale Server - Calendar Server
|
||||
# This file is part of Radicale - CalDAV and CardDAV server
|
||||
# Copyright © 2008 Nicolas Kandel
|
||||
# Copyright © 2008 Pascal Halter
|
||||
# Copyright © 2008-2017 Guillaume Ayoub
|
||||
|
@ -20,17 +20,22 @@
|
|||
import contextlib
|
||||
import posixpath
|
||||
import socket
|
||||
import xml.etree.ElementTree as ET
|
||||
from http import client
|
||||
from typing import Callable, Iterable, Iterator, Optional, Sequence, Tuple
|
||||
from urllib.parse import unquote, urlparse
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from radicale import app, httputils, pathutils, storage, xmlutils
|
||||
import radicale.item as radicale_item
|
||||
from radicale import httputils, pathutils, storage, types, xmlutils
|
||||
from radicale.app.base import Access, ApplicationBase
|
||||
from radicale.item import filter as radicale_filter
|
||||
from radicale.log import logger
|
||||
|
||||
|
||||
def xml_report(base_prefix, path, xml_request, collection, encoding,
|
||||
unlock_storage_fn):
|
||||
def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element],
|
||||
collection: storage.BaseCollection, encoding: str,
|
||||
unlock_storage_fn: Callable[[], None]
|
||||
) -> Tuple[int, ET.Element]:
|
||||
"""Read and answer REPORT requests.
|
||||
|
||||
Read rfc3253-3.6 for info.
|
||||
|
@ -40,10 +45,9 @@ def xml_report(base_prefix, path, xml_request, collection, encoding,
|
|||
if xml_request is None:
|
||||
return client.MULTI_STATUS, multistatus
|
||||
root = xml_request
|
||||
if root.tag in (
|
||||
xmlutils.make_clark("D:principal-search-property-set"),
|
||||
xmlutils.make_clark("D:principal-property-search"),
|
||||
xmlutils.make_clark("D:expand-property")):
|
||||
if root.tag in (xmlutils.make_clark("D:principal-search-property-set"),
|
||||
xmlutils.make_clark("D:principal-property-search"),
|
||||
xmlutils.make_clark("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
|
||||
|
@ -52,28 +56,28 @@ def xml_report(base_prefix, path, xml_request, collection, encoding,
|
|||
xmlutils.make_human_tag(root.tag), path)
|
||||
return client.MULTI_STATUS, multistatus
|
||||
if (root.tag == xmlutils.make_clark("C:calendar-multiget") and
|
||||
collection.get_meta("tag") != "VCALENDAR" or
|
||||
collection.tag != "VCALENDAR" or
|
||||
root.tag == xmlutils.make_clark("CR:addressbook-multiget") and
|
||||
collection.get_meta("tag") != "VADDRESSBOOK" or
|
||||
collection.tag != "VADDRESSBOOK" or
|
||||
root.tag == xmlutils.make_clark("D:sync-collection") and
|
||||
collection.get_meta("tag") not in ("VADDRESSBOOK", "VCALENDAR")):
|
||||
collection.tag not in ("VADDRESSBOOK", "VCALENDAR")):
|
||||
logger.warning("Invalid REPORT method %r on %r requested",
|
||||
xmlutils.make_human_tag(root.tag), path)
|
||||
return (client.FORBIDDEN,
|
||||
xmlutils.webdav_error("D:supported-report"))
|
||||
return client.FORBIDDEN, xmlutils.webdav_error("D:supported-report")
|
||||
prop_element = root.find(xmlutils.make_clark("D:prop"))
|
||||
props = (
|
||||
[prop.tag for prop in prop_element]
|
||||
if prop_element is not None else [])
|
||||
props = ([prop.tag for prop in prop_element]
|
||||
if prop_element is not None else [])
|
||||
|
||||
hreferences: Iterable[str]
|
||||
if root.tag in (
|
||||
xmlutils.make_clark("C:calendar-multiget"),
|
||||
xmlutils.make_clark("CR:addressbook-multiget")):
|
||||
# Read rfc4791-7.9 for info
|
||||
hreferences = set()
|
||||
for href_element in root.findall(xmlutils.make_clark("D:href")):
|
||||
href_path = pathutils.sanitize_path(
|
||||
unquote(urlparse(href_element.text).path))
|
||||
temp_url_path = urlparse(href_element.text).path
|
||||
assert isinstance(temp_url_path, str)
|
||||
href_path = pathutils.sanitize_path(unquote(temp_url_path))
|
||||
if (href_path + "/").startswith(base_prefix + "/"):
|
||||
hreferences.add(href_path[len(base_prefix):])
|
||||
else:
|
||||
|
@ -104,85 +108,16 @@ def xml_report(base_prefix, path, xml_request, collection, encoding,
|
|||
else:
|
||||
hreferences = (path,)
|
||||
filters = (
|
||||
root.findall("./%s" % xmlutils.make_clark("C:filter")) +
|
||||
root.findall("./%s" % xmlutils.make_clark("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)
|
||||
root.findall(xmlutils.make_clark("C:filter")) +
|
||||
root.findall(xmlutils.make_clark("CR:filter")))
|
||||
|
||||
# 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!
|
||||
retrieved_items = list(retrieve_items(
|
||||
base_prefix, path, collection, hreferences, filters, multistatus))
|
||||
collection_tag = collection.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_clark("C:%s" % 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_clark("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_clark("CR:%s" % filter_)):
|
||||
for child in filter_:
|
||||
if child.tag != xmlutils.make_clark("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)
|
||||
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
|
||||
|
@ -190,7 +125,8 @@ def xml_report(base_prefix, path, xml_request, collection, encoding,
|
|||
item, filters_matched = retrieved_items.pop(0)
|
||||
if filters and not filters_matched:
|
||||
try:
|
||||
if not all(match(item, filter_) for filter_ in filters):
|
||||
if not all(test_filter(collection_tag, item, filter_)
|
||||
for filter_ in filters):
|
||||
continue
|
||||
except ValueError as e:
|
||||
raise ValueError("Failed to filter item %r from %r: %s" %
|
||||
|
@ -218,6 +154,7 @@ def xml_report(base_prefix, path, xml_request, collection, encoding,
|
|||
else:
|
||||
not_found_props.append(element)
|
||||
|
||||
assert item.href
|
||||
uri = pathutils.unstrip_path(
|
||||
posixpath.join(collection.path, item.href))
|
||||
multistatus.append(xml_item_response(
|
||||
|
@ -227,13 +164,15 @@ def xml_report(base_prefix, path, xml_request, collection, encoding,
|
|||
return client.MULTI_STATUS, multistatus
|
||||
|
||||
|
||||
def xml_item_response(base_prefix, href, found_props=(), not_found_props=(),
|
||||
found_item=True):
|
||||
def xml_item_response(base_prefix: str, href: str,
|
||||
found_props: Sequence[ET.Element] = (),
|
||||
not_found_props: Sequence[ET.Element] = (),
|
||||
found_item: bool = True) -> ET.Element:
|
||||
response = ET.Element(xmlutils.make_clark("D:response"))
|
||||
|
||||
href_tag = ET.Element(xmlutils.make_clark("D:href"))
|
||||
href_tag.text = xmlutils.make_href(base_prefix, href)
|
||||
response.append(href_tag)
|
||||
href_element = ET.Element(xmlutils.make_clark("D:href"))
|
||||
href_element.text = xmlutils.make_href(base_prefix, href)
|
||||
response.append(href_element)
|
||||
|
||||
if found_item:
|
||||
for code, props in ((200, found_props), (404, not_found_props)):
|
||||
|
@ -241,10 +180,10 @@ def xml_item_response(base_prefix, href, found_props=(), not_found_props=(),
|
|||
propstat = ET.Element(xmlutils.make_clark("D:propstat"))
|
||||
status = ET.Element(xmlutils.make_clark("D:status"))
|
||||
status.text = xmlutils.make_response(code)
|
||||
prop_tag = ET.Element(xmlutils.make_clark("D:prop"))
|
||||
prop_element = ET.Element(xmlutils.make_clark("D:prop"))
|
||||
for prop in props:
|
||||
prop_tag.append(prop)
|
||||
propstat.append(prop_tag)
|
||||
prop_element.append(prop)
|
||||
propstat.append(prop_element)
|
||||
propstat.append(status)
|
||||
response.append(propstat)
|
||||
else:
|
||||
|
@ -255,24 +194,98 @@ def xml_item_response(base_prefix, href, found_props=(), not_found_props=(),
|
|||
return response
|
||||
|
||||
|
||||
class ApplicationReportMixin:
|
||||
def do_REPORT(self, environ, base_prefix, path, user):
|
||||
def retrieve_items(
|
||||
base_prefix: str, path: str, collection: storage.BaseCollection,
|
||||
hreferences: Iterable[str], filters: Sequence[ET.Element],
|
||||
multistatus: ET.Element) -> Iterator[Tuple[radicale_item.Item, bool]]:
|
||||
"""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() -> Iterator[str]:
|
||||
"""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)
|
||||
|
||||
|
||||
def test_filter(collection_tag: str, item: radicale_item.Item,
|
||||
filter_: ET.Element) -> bool:
|
||||
"""Match an item against a filter."""
|
||||
if (collection_tag == "VCALENDAR" and
|
||||
filter_.tag != xmlutils.make_clark("C:%s" % 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_clark("C:comp-filter"):
|
||||
raise ValueError("Unexpected %r in filter" % filter_[0].tag)
|
||||
return radicale_filter.comp_match(item, filter_[0])
|
||||
if (collection_tag == "VADDRESSBOOK" and
|
||||
filter_.tag != xmlutils.make_clark("CR:%s" % filter_)):
|
||||
for child in filter_:
|
||||
if child.tag != xmlutils.make_clark("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)
|
||||
raise ValueError("Unsupported filter %r for %r" %
|
||||
(filter_.tag, collection_tag))
|
||||
|
||||
|
||||
class ApplicationPartReport(ApplicationBase):
|
||||
|
||||
def do_REPORT(self, environ: types.WSGIEnviron, base_prefix: str,
|
||||
path: str, user: str) -> types.WSGIResponse:
|
||||
"""Manage REPORT request."""
|
||||
access = app.Access(self._rights, user, path)
|
||||
access = Access(self._rights, user, path)
|
||||
if not access.check("r"):
|
||||
return httputils.NOT_ALLOWED
|
||||
try:
|
||||
xml_content = self._read_xml_content(environ)
|
||||
xml_content = self._read_xml_request_body(environ)
|
||||
except RuntimeError as e:
|
||||
logger.warning(
|
||||
"Bad REPORT request on %r: %s", path, e, exc_info=True)
|
||||
logger.warning("Bad REPORT request on %r: %s", path, e,
|
||||
exc_info=True)
|
||||
return httputils.BAD_REQUEST
|
||||
except socket.timeout:
|
||||
logger.debug("client timed out", exc_info=True)
|
||||
logger.debug("Client timed out", exc_info=True)
|
||||
return httputils.REQUEST_TIMEOUT
|
||||
with contextlib.ExitStack() as lock_stack:
|
||||
lock_stack.enter_context(self._storage.acquire_lock("r", user))
|
||||
item = next(self._storage.discover(path), None)
|
||||
item = next(iter(self._storage.discover(path)), None)
|
||||
if not item:
|
||||
return httputils.NOT_FOUND
|
||||
if not access.check("r", item):
|
||||
|
@ -280,8 +293,8 @@ class ApplicationReportMixin:
|
|||
if isinstance(item, storage.BaseCollection):
|
||||
collection = item
|
||||
else:
|
||||
assert item.collection is not None
|
||||
collection = item.collection
|
||||
headers = {"Content-Type": "text/xml; charset=%s" % self._encoding}
|
||||
try:
|
||||
status, xml_answer = xml_report(
|
||||
base_prefix, path, xml_content, collection, self._encoding,
|
||||
|
@ -290,4 +303,5 @@ class ApplicationReportMixin:
|
|||
logger.warning(
|
||||
"Bad REPORT request on %r: %s", path, e, exc_info=True)
|
||||
return httputils.BAD_REQUEST
|
||||
return (status, headers, self._write_xml_content(xml_answer))
|
||||
headers = {"Content-Type": "text/xml; charset=%s" % self._encoding}
|
||||
return status, headers, self._xml_response(xml_answer)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue