proppatch.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. # This file is part of Radicale - CalDAV and CardDAV server
  2. # Copyright © 2008 Nicolas Kandel
  3. # Copyright © 2008 Pascal Halter
  4. # Copyright © 2008-2017 Guillaume Ayoub
  5. # Copyright © 2017-2020 Unrud <unrud@outlook.com>
  6. # Copyright © 2020-2020 Tuna Celik <tuna@jakpark.com>
  7. # Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de>
  8. #
  9. # This library is free software: you can redistribute it and/or modify
  10. # it under the terms of the GNU General Public License as published by
  11. # the Free Software Foundation, either version 3 of the License, or
  12. # (at your option) any later version.
  13. #
  14. # This library is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License
  20. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  21. import errno
  22. import re
  23. import socket
  24. import xml.etree.ElementTree as ET
  25. from http import client
  26. from typing import Dict, Optional, cast
  27. import defusedxml.ElementTree as DefusedET
  28. import radicale.item as radicale_item
  29. from radicale import httputils, storage, types, xmlutils
  30. from radicale.app.base import Access, ApplicationBase
  31. from radicale.hook import HookNotificationItem, HookNotificationItemTypes
  32. from radicale.log import logger
  33. def xml_proppatch(base_prefix: str, path: str,
  34. xml_request: Optional[ET.Element],
  35. collection: storage.BaseCollection) -> ET.Element:
  36. """Read and answer PROPPATCH requests.
  37. Read rfc4918-9.2 for info.
  38. """
  39. multistatus = ET.Element(xmlutils.make_clark("D:multistatus"))
  40. response = ET.Element(xmlutils.make_clark("D:response"))
  41. multistatus.append(response)
  42. href = ET.Element(xmlutils.make_clark("D:href"))
  43. href.text = xmlutils.make_href(base_prefix, path)
  44. response.append(href)
  45. # Create D:propstat element for props with status 200 OK
  46. propstat = ET.Element(xmlutils.make_clark("D:propstat"))
  47. status = ET.Element(xmlutils.make_clark("D:status"))
  48. status.text = xmlutils.make_response(200)
  49. props_ok = ET.Element(xmlutils.make_clark("D:prop"))
  50. propstat.append(props_ok)
  51. propstat.append(status)
  52. response.append(propstat)
  53. props_with_remove = xmlutils.props_from_request(xml_request)
  54. all_props_with_remove = cast(Dict[str, Optional[str]],
  55. dict(collection.get_meta()))
  56. all_props_with_remove.update(props_with_remove)
  57. all_props = radicale_item.check_and_sanitize_props(all_props_with_remove)
  58. collection.set_meta(all_props)
  59. for short_name in props_with_remove:
  60. props_ok.append(ET.Element(xmlutils.make_clark(short_name)))
  61. return multistatus
  62. class ApplicationPartProppatch(ApplicationBase):
  63. def do_PROPPATCH(self, environ: types.WSGIEnviron, base_prefix: str,
  64. path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
  65. """Manage PROPPATCH request."""
  66. access = Access(self._rights, user, path)
  67. if not access.check("w"):
  68. return httputils.NOT_ALLOWED
  69. try:
  70. xml_content = self._read_xml_request_body(environ)
  71. except RuntimeError as e:
  72. logger.warning(
  73. "Bad PROPPATCH request on %r: %s", path, e, exc_info=True)
  74. return httputils.BAD_REQUEST
  75. except socket.timeout:
  76. logger.debug("Client timed out", exc_info=True)
  77. return httputils.REQUEST_TIMEOUT
  78. with self._storage.acquire_lock("w", user, path=path, request="PROPPATCH"):
  79. item = next(iter(self._storage.discover(path)), None)
  80. if not item:
  81. return httputils.NOT_FOUND
  82. if not access.check("w", item):
  83. return httputils.NOT_ALLOWED
  84. if not isinstance(item, storage.BaseCollection):
  85. return httputils.FORBIDDEN
  86. headers = {"DAV": httputils.DAV_HEADERS,
  87. "Content-Type": "text/xml; charset=%s" % self._encoding}
  88. try:
  89. xml_answer = xml_proppatch(base_prefix, path, xml_content,
  90. item)
  91. if xml_content is not None:
  92. content = DefusedET.tostring(
  93. xml_content,
  94. encoding=self._encoding
  95. ).decode(encoding=self._encoding)
  96. hook_notification_item = HookNotificationItem(
  97. notification_item_type=HookNotificationItemTypes.CPATCH,
  98. path=access.path,
  99. content=content,
  100. uid=None,
  101. old_content=None,
  102. new_content=content
  103. )
  104. self._hook.notify(hook_notification_item)
  105. except ValueError as e:
  106. # return better matching HTTP result in case errno is provided and catched
  107. errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e))
  108. if errno_match:
  109. logger.error(
  110. "Failed PROPPATCH request on %r: %s", path, e, exc_info=True)
  111. errno_e = int(errno_match.group(1))
  112. if errno_e == errno.ENOSPC:
  113. return httputils.INSUFFICIENT_STORAGE
  114. elif errno_e in [errno.EPERM, errno.EACCES]:
  115. return httputils.FORBIDDEN
  116. else:
  117. return httputils.INTERNAL_SERVER_ERROR
  118. else:
  119. logger.warning(
  120. "Bad PROPPATCH request on %r: %s", path, e, exc_info=True)
  121. return httputils.BAD_REQUEST
  122. return client.MULTI_STATUS, headers, self._xml_response(xml_answer), xmlutils.pretty_xml(xml_content)