1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-06-26 16:45:52 +00:00
Radicale/radicale/app/propfind.py

413 lines
18 KiB
Python
Raw Permalink Normal View History

2021-12-08 21:45:42 +01:00
# This file is part of Radicale - CalDAV and CardDAV server
2018-08-28 16:19:36 +02:00
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
2019-06-17 04:13:24 +02:00
# Copyright © 2017-2018 Unrud <unrud@outlook.com>
2018-08-28 16:19:36 +02:00
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
import collections
2018-08-28 16:19:36 +02:00
import itertools
2019-06-15 09:01:55 +02:00
import posixpath
2018-08-28 16:19:36 +02:00
import socket
2020-10-04 10:14:57 +02:00
import xml.etree.ElementTree as ET
2018-08-28 16:19:36 +02:00
from http import client
2021-07-26 20:56:46 +02:00
from typing import Dict, Iterable, Iterator, List, Optional, Sequence, Tuple
2018-08-28 16:19:36 +02:00
2021-07-26 20:56:46 +02:00
from radicale import httputils, pathutils, rights, storage, types, xmlutils
from radicale.app.base import Access, ApplicationBase
2018-08-28 16:19:36 +02:00
from radicale.log import logger
2021-07-26 20:56:46 +02:00
def xml_propfind(base_prefix: str, path: str,
xml_request: Optional[ET.Element],
allowed_items: Iterable[Tuple[types.CollectionOrItem, str]],
user: str, encoding: str) -> Optional[ET.Element]:
2018-08-28 16:19:36 +02:00
"""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.
2020-09-26 22:08:21 +02:00
top_element = (xml_request[0] if xml_request is not None else
ET.Element(xmlutils.make_clark("D:allprop")))
2018-08-28 16:19:36 +02:00
2021-07-26 20:56:46 +02:00
props: List[str] = []
2018-08-28 16:19:36 +02:00
allprop = False
propname = False
2020-09-26 22:08:21 +02:00
if top_element.tag == xmlutils.make_clark("D:allprop"):
2018-08-28 16:19:36 +02:00
allprop = True
2020-09-26 22:08:21 +02:00
elif top_element.tag == xmlutils.make_clark("D:propname"):
2018-08-28 16:19:36 +02:00
propname = True
2020-09-26 22:08:21 +02:00
elif top_element.tag == xmlutils.make_clark("D:prop"):
2021-07-26 20:56:46 +02:00
props.extend(prop.tag for prop in top_element)
2018-08-28 16:19:36 +02:00
if xmlutils.make_clark("D:current-user-principal") in props and not user:
2018-08-28 16:19:36 +02:00
# Ask for authentication
# Returning the DAV:unauthenticated pseudo-principal as specified in
# RFC 5397 doesn't seem to work with DAVx5.
2021-07-26 20:56:46 +02:00
return None
2018-08-28 16:19:36 +02:00
# Writing answer
multistatus = ET.Element(xmlutils.make_clark("D:multistatus"))
2018-08-28 16:19:36 +02:00
for item, permission in allowed_items:
write = permission == "w"
multistatus.append(xml_propfind_response(
base_prefix, path, item, props, user, encoding, write=write,
allprop=allprop, propname=propname))
2018-08-28 16:19:36 +02:00
2021-07-26 20:56:46 +02:00
return multistatus
2018-08-28 16:19:36 +02:00
2021-07-26 20:56:46 +02:00
def xml_propfind_response(
base_prefix: str, path: str, item: types.CollectionOrItem,
props: Sequence[str], user: str, encoding: str, write: bool = False,
propname: bool = False, allprop: bool = False) -> ET.Element:
2018-08-28 16:19:36 +02:00
"""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")
2021-07-26 20:56:46 +02:00
if isinstance(item, storage.BaseCollection):
is_collection = True
2022-04-12 09:50:05 +02:00
is_leaf = item.tag in ("VADDRESSBOOK", "VCALENDAR", "VSUBSCRIBED")
2018-08-28 16:19:36 +02:00
collection = item
2021-07-26 20:56:46 +02:00
# Some clients expect collections to end with `/`
uri = pathutils.unstrip_path(item.path, True)
2018-08-28 16:19:36 +02:00
else:
2021-07-26 20:56:46 +02:00
is_collection = is_leaf = False
assert item.collection is not None
assert item.href
2018-08-28 16:19:36 +02:00
collection = item.collection
2021-07-26 20:56:46 +02:00
uri = pathutils.unstrip_path(posixpath.join(
collection.path, item.href))
response = ET.Element(xmlutils.make_clark("D:response"))
href = ET.Element(xmlutils.make_clark("D:href"))
2018-08-28 16:19:36 +02:00
href.text = xmlutils.make_href(base_prefix, uri)
response.append(href)
if propname or allprop:
props = []
# Should list all properties that can be retrieved by the code below
props.append(xmlutils.make_clark("D:principal-collection-set"))
props.append(xmlutils.make_clark("D:current-user-principal"))
props.append(xmlutils.make_clark("D:current-user-privilege-set"))
props.append(xmlutils.make_clark("D:supported-report-set"))
props.append(xmlutils.make_clark("D:resourcetype"))
props.append(xmlutils.make_clark("D:owner"))
2018-08-28 16:19:36 +02:00
if is_collection and collection.is_principal:
props.append(xmlutils.make_clark("C:calendar-user-address-set"))
props.append(xmlutils.make_clark("D:principal-URL"))
props.append(xmlutils.make_clark("CR:addressbook-home-set"))
props.append(xmlutils.make_clark("C:calendar-home-set"))
2018-08-28 16:19:36 +02:00
if not is_collection or is_leaf:
props.append(xmlutils.make_clark("D:getetag"))
props.append(xmlutils.make_clark("D:getlastmodified"))
props.append(xmlutils.make_clark("D:getcontenttype"))
props.append(xmlutils.make_clark("D:getcontentlength"))
2018-08-28 16:19:36 +02:00
if is_collection:
if is_leaf:
props.append(xmlutils.make_clark("D:displayname"))
props.append(xmlutils.make_clark("D:sync-token"))
2021-07-26 20:56:46 +02:00
if collection.tag == "VCALENDAR":
props.append(xmlutils.make_clark("CS:getctag"))
2018-08-28 16:19:36 +02:00
props.append(
xmlutils.make_clark("C:supported-calendar-component-set"))
2018-08-28 16:19:36 +02:00
2021-07-26 20:56:46 +02:00
meta = collection.get_meta()
2018-08-28 16:19:36 +02:00
for tag in meta:
if tag == "tag":
continue
clark_tag = xmlutils.make_clark(tag)
2018-08-28 16:19:36 +02:00
if clark_tag not in props:
props.append(clark_tag)
2021-07-26 20:56:46 +02:00
responses: Dict[int, List[ET.Element]] = collections.defaultdict(list)
2018-08-28 16:19:36 +02:00
if propname:
for tag in props:
responses[200].append(ET.Element(tag))
2021-07-26 20:56:46 +02:00
props = []
2018-08-28 16:19:36 +02:00
for tag in props:
element = ET.Element(tag)
is404 = False
if tag == xmlutils.make_clark("D:getetag"):
2018-08-28 16:19:36 +02:00
if not is_collection or is_leaf:
element.text = item.etag
else:
is404 = True
elif tag == xmlutils.make_clark("D:getlastmodified"):
2018-08-28 16:19:36 +02:00
if not is_collection or is_leaf:
element.text = item.last_modified
else:
is404 = True
elif tag == xmlutils.make_clark("D:principal-collection-set"):
2020-09-26 22:08:21 +02:00
child_element = ET.Element(xmlutils.make_clark("D:href"))
child_element.text = xmlutils.make_href(base_prefix, "/")
element.append(child_element)
elif (tag in (xmlutils.make_clark("C:calendar-user-address-set"),
xmlutils.make_clark("D:principal-URL"),
xmlutils.make_clark("CR:addressbook-home-set"),
xmlutils.make_clark("C:calendar-home-set")) and
2021-07-26 20:56:46 +02:00
is_collection and collection.is_principal):
2020-09-26 22:08:21 +02:00
child_element = ET.Element(xmlutils.make_clark("D:href"))
child_element.text = xmlutils.make_href(base_prefix, path)
element.append(child_element)
elif tag == xmlutils.make_clark("C:supported-calendar-component-set"):
human_tag = xmlutils.make_human_tag(tag)
2018-08-28 16:19:36 +02:00
if is_collection and is_leaf:
2021-07-26 20:56:46 +02:00
components_text = collection.get_meta(human_tag)
if components_text:
components = components_text.split(",")
2018-08-28 16:19:36 +02:00
else:
2021-07-26 20:56:46 +02:00
components = ["VTODO", "VEVENT", "VJOURNAL"]
2018-08-28 16:19:36 +02:00
for component in components:
comp = ET.Element(xmlutils.make_clark("C:comp"))
2018-08-28 16:19:36 +02:00
comp.set("name", component)
element.append(comp)
else:
is404 = True
elif tag == xmlutils.make_clark("D:current-user-principal"):
2018-08-28 16:19:36 +02:00
if user:
2020-09-26 22:08:21 +02:00
child_element = ET.Element(xmlutils.make_clark("D:href"))
child_element.text = xmlutils.make_href(
base_prefix, "/%s/" % user)
element.append(child_element)
2018-08-28 16:19:36 +02:00
else:
element.append(ET.Element(
xmlutils.make_clark("D:unauthenticated")))
elif tag == xmlutils.make_clark("D:current-user-privilege-set"):
privileges = ["D:read"]
2018-08-28 16:19:36 +02:00
if write:
privileges.append("D:all")
privileges.append("D:write")
privileges.append("D:write-properties")
privileges.append("D:write-content")
for human_tag in privileges:
privilege = ET.Element(xmlutils.make_clark("D:privilege"))
2018-08-28 16:19:36 +02:00
privilege.append(ET.Element(
xmlutils.make_clark(human_tag)))
2018-08-28 16:19:36 +02:00
element.append(privilege)
elif tag == xmlutils.make_clark("D:supported-report-set"):
2018-08-28 16:19:36 +02:00
# These 3 reports are not implemented
reports = ["D:expand-property",
"D:principal-search-property-set",
"D:principal-property-search"]
2018-08-28 16:19:36 +02:00
if is_collection and is_leaf:
reports.append("D:sync-collection")
2021-07-26 20:56:46 +02:00
if collection.tag == "VADDRESSBOOK":
reports.append("CR:addressbook-multiget")
reports.append("CR:addressbook-query")
2021-07-26 20:56:46 +02:00
elif collection.tag == "VCALENDAR":
reports.append("C:calendar-multiget")
reports.append("C:calendar-query")
for human_tag in reports:
supported_report = ET.Element(
xmlutils.make_clark("D:supported-report"))
2020-09-26 22:08:21 +02:00
report_element = ET.Element(xmlutils.make_clark("D:report"))
report_element.append(
ET.Element(xmlutils.make_clark(human_tag)))
supported_report.append(report_element)
element.append(supported_report)
elif tag == xmlutils.make_clark("D:getcontentlength"):
2018-08-28 16:19:36 +02:00
if not is_collection or is_leaf:
element.text = str(len(item.serialize().encode(encoding)))
else:
is404 = True
elif tag == xmlutils.make_clark("D:owner"):
2018-08-28 16:19:36 +02:00
# return empty elment, if no owner available (rfc3744-5.1)
if collection.owner:
2020-09-26 22:08:21 +02:00
child_element = ET.Element(xmlutils.make_clark("D:href"))
child_element.text = xmlutils.make_href(
2018-08-28 16:19:36 +02:00
base_prefix, "/%s/" % collection.owner)
2020-09-26 22:08:21 +02:00
element.append(child_element)
2018-08-28 16:19:36 +02:00
elif is_collection:
if tag == xmlutils.make_clark("D:getcontenttype"):
2018-08-28 16:19:36 +02:00
if is_leaf:
2021-07-26 20:56:46 +02:00
element.text = xmlutils.MIMETYPES[
collection.tag]
2018-08-28 16:19:36 +02:00
else:
is404 = True
elif tag == xmlutils.make_clark("D:resourcetype"):
2021-07-26 20:56:46 +02:00
if collection.is_principal:
2020-09-26 22:08:21 +02:00
child_element = ET.Element(
xmlutils.make_clark("D:principal"))
element.append(child_element)
2018-08-28 16:19:36 +02:00
if is_leaf:
2021-07-26 20:56:46 +02:00
if collection.tag == "VADDRESSBOOK":
2020-09-26 22:08:21 +02:00
child_element = ET.Element(
xmlutils.make_clark("CR:addressbook"))
2020-09-26 22:08:21 +02:00
element.append(child_element)
2021-07-26 20:56:46 +02:00
elif collection.tag == "VCALENDAR":
2020-09-26 22:08:21 +02:00
child_element = ET.Element(
xmlutils.make_clark("C:calendar"))
element.append(child_element)
2022-04-12 09:50:05 +02:00
elif collection.tag == "VSUBSCRIBED":
child_element = ET.Element(
xmlutils.make_clark("CS:subscribed"))
element.append(child_element)
2020-09-26 22:08:21 +02:00
child_element = ET.Element(xmlutils.make_clark("D:collection"))
element.append(child_element)
elif tag == xmlutils.make_clark("RADICALE:displayname"):
2018-08-28 16:19:36 +02:00
# Only for internal use by the web interface
2021-07-26 20:56:46 +02:00
displayname = collection.get_meta("D:displayname")
2018-08-28 16:19:36 +02:00
if displayname is not None:
element.text = displayname
else:
is404 = True
elif tag == xmlutils.make_clark("RADICALE:getcontentcount"):
# Only for internal use by the web interface
if isinstance(item, storage.BaseCollection) and not collection.is_principal:
element.text = str(sum(1 for x in item.get_all()))
else:
is404 = True
elif tag == xmlutils.make_clark("D:displayname"):
2021-07-26 20:56:46 +02:00
displayname = collection.get_meta("D:displayname")
2018-08-28 16:19:36 +02:00
if not displayname and is_leaf:
2021-07-26 20:56:46 +02:00
displayname = collection.path
2018-08-28 16:19:36 +02:00
if displayname is not None:
element.text = displayname
else:
is404 = True
elif tag == xmlutils.make_clark("CS:getctag"):
2018-08-28 16:19:36 +02:00
if is_leaf:
2021-07-26 20:56:46 +02:00
element.text = collection.etag
2018-08-28 16:19:36 +02:00
else:
is404 = True
elif tag == xmlutils.make_clark("D:sync-token"):
2018-08-28 16:19:36 +02:00
if is_leaf:
2021-07-26 20:56:46 +02:00
element.text, _ = collection.sync()
2018-08-28 16:19:36 +02:00
else:
is404 = True
2022-04-12 09:50:05 +02:00
elif tag == xmlutils.make_clark("CS:source"):
if is_leaf:
child_element = ET.Element(xmlutils.make_clark("D:href"))
child_element.text = collection.get_meta('CS:source')
element.append(child_element)
else:
is404 = True
2018-08-28 16:19:36 +02:00
else:
human_tag = xmlutils.make_human_tag(tag)
2021-07-26 20:56:46 +02:00
tag_text = collection.get_meta(human_tag)
if tag_text is not None:
element.text = tag_text
2018-08-28 16:19:36 +02:00
else:
is404 = True
# Not for collections
elif tag == xmlutils.make_clark("D:getcontenttype"):
2021-07-26 20:56:46 +02:00
assert not isinstance(item, storage.BaseCollection)
element.text = xmlutils.get_content_type(item, encoding)
elif tag == xmlutils.make_clark("D:resourcetype"):
2018-08-28 16:19:36 +02:00
# resourcetype must be returned empty for non-collection elements
pass
else:
is404 = True
responses[404 if is404 else 200].append(element)
2024-07-24 11:22:49 +02:00
for status_code, children in responses.items():
if not children:
continue
propstat = ET.Element(xmlutils.make_clark("D:propstat"))
response.append(propstat)
prop = ET.Element(xmlutils.make_clark("D:prop"))
2024-07-24 11:22:49 +02:00
prop.extend(children)
propstat.append(prop)
status = ET.Element(xmlutils.make_clark("D:status"))
status.text = xmlutils.make_response(status_code)
propstat.append(status)
2018-08-28 16:19:36 +02:00
return response
2021-07-26 20:56:46 +02:00
class ApplicationPartPropfind(ApplicationBase):
def _collect_allowed_items(
self, items: Iterable[types.CollectionOrItem], user: str
) -> Iterator[Tuple[types.CollectionOrItem, str]]:
2018-08-28 16:19:36 +02:00
"""Get items from request that user is allowed to access."""
for item in items:
if isinstance(item, storage.BaseCollection):
2018-08-28 16:19:50 +02:00
path = pathutils.unstrip_path(item.path, True)
2021-07-26 20:56:46 +02:00
if item.tag:
2020-04-09 22:02:03 +02:00
permissions = rights.intersect(
self._rights.authorization(user, path), "rw")
2018-08-28 16:19:36 +02:00
target = "collection with tag %r" % item.path
else:
2020-04-09 22:02:03 +02:00
permissions = rights.intersect(
self._rights.authorization(user, path), "RW")
2018-08-28 16:19:36 +02:00
target = "collection %r" % item.path
else:
2021-07-26 20:56:46 +02:00
assert item.collection is not None
2018-08-28 16:19:50 +02:00
path = pathutils.unstrip_path(item.collection.path, True)
2020-04-09 22:02:03 +02:00
permissions = rights.intersect(
self._rights.authorization(user, path), "rw")
2018-08-28 16:19:36 +02:00
target = "item %r from %r" % (item.href, item.collection.path)
2020-04-09 22:02:03 +02:00
if rights.intersect(permissions, "Ww"):
2018-08-28 16:19:36 +02:00
permission = "w"
status = "write"
2020-04-09 22:02:03 +02:00
elif rights.intersect(permissions, "Rr"):
2018-08-28 16:19:36 +02:00
permission = "r"
status = "read"
else:
permission = ""
status = "NO"
logger.debug(
"%s has %s access to %s",
repr(user) if user else "anonymous user", status, target)
if permission:
yield item, permission
2021-07-26 20:56:46 +02:00
def do_PROPFIND(self, environ: types.WSGIEnviron, base_prefix: str,
path: str, user: str) -> types.WSGIResponse:
2018-08-28 16:19:36 +02:00
"""Manage PROPFIND request."""
2021-07-26 20:56:46 +02:00
access = Access(self._rights, user, path)
2020-04-22 19:20:07 +02:00
if not access.check("r"):
2018-08-28 16:19:36 +02:00
return httputils.NOT_ALLOWED
try:
2020-09-14 21:19:48 +02:00
xml_content = self._read_xml_request_body(environ)
2018-08-28 16:19:36 +02:00
except RuntimeError as e:
logger.warning(
"Bad PROPFIND request on %r: %s", path, e, exc_info=True)
return httputils.BAD_REQUEST
2018-11-04 18:54:11 +00:00
except socket.timeout:
logger.debug("Client timed out", exc_info=True)
2018-08-28 16:19:36 +02:00
return httputils.REQUEST_TIMEOUT
with self._storage.acquire_lock("r", user):
2021-07-26 20:56:46 +02:00
items_iter = iter(self._storage.discover(
path, environ.get("HTTP_DEPTH", "0"),
None, self._rights._user_groups))
2018-08-28 16:19:36 +02:00
# take root item for rights checking
2021-07-26 20:56:46 +02:00
item = next(items_iter, None)
2018-08-28 16:19:36 +02:00
if not item:
return httputils.NOT_FOUND
2020-04-22 19:20:07 +02:00
if not access.check("r", item):
2018-08-28 16:19:36 +02:00
return httputils.NOT_ALLOWED
# put item back
2021-07-26 20:56:46 +02:00
items_iter = itertools.chain([item], items_iter)
allowed_items = self._collect_allowed_items(items_iter, user)
2018-08-28 16:19:36 +02:00
headers = {"DAV": httputils.DAV_HEADERS,
"Content-Type": "text/xml; charset=%s" % self._encoding}
2021-07-26 20:56:46 +02:00
xml_answer = xml_propfind(base_prefix, path, xml_content,
allowed_items, user, self._encoding)
if xml_answer is None:
2018-08-28 16:19:36 +02:00
return httputils.NOT_ALLOWED
2021-07-26 20:56:46 +02:00
return client.MULTI_STATUS, headers, self._xml_response(xml_answer)