put.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  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-2023 Tuna Celik <tuna@jakpark.com>
  7. # Copyright © 2024-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 itertools
  23. import posixpath
  24. import re
  25. import socket
  26. import sys
  27. from http import client
  28. from types import TracebackType
  29. from typing import Iterator, List, Mapping, MutableMapping, Optional, Tuple
  30. import vobject
  31. import radicale.item as radicale_item
  32. from radicale import (httputils, pathutils, rights, storage, types, utils,
  33. xmlutils)
  34. from radicale.app.base import Access, ApplicationBase
  35. from radicale.hook import HookNotificationItem, HookNotificationItemTypes
  36. from radicale.log import logger
  37. MIMETYPE_TAGS: Mapping[str, str] = {value: key for key, value in
  38. xmlutils.MIMETYPES.items()}
  39. PRODID = u"-//Radicale//NONSGML Version " + utils.package_version("radicale") + "//EN"
  40. def prepare(vobject_items: List[vobject.base.Component], path: str,
  41. content_type: str, permission: bool, parent_permission: bool,
  42. tag: Optional[str] = None,
  43. write_whole_collection: Optional[bool] = None) -> Tuple[
  44. Iterator[radicale_item.Item], # items
  45. Optional[str], # tag
  46. Optional[bool], # write_whole_collection
  47. Optional[MutableMapping[str, str]], # props
  48. Optional[Tuple[type, BaseException, Optional[TracebackType]]]]:
  49. if (write_whole_collection or permission and not parent_permission):
  50. write_whole_collection = True
  51. tag = radicale_item.predict_tag_of_whole_collection(
  52. vobject_items, MIMETYPE_TAGS.get(content_type))
  53. if not tag:
  54. raise ValueError("Can't determine collection tag")
  55. collection_path = pathutils.strip_path(path)
  56. elif (write_whole_collection is not None and not write_whole_collection or
  57. not permission and parent_permission):
  58. write_whole_collection = False
  59. if tag is None:
  60. tag = radicale_item.predict_tag_of_parent_collection(vobject_items)
  61. collection_path = posixpath.dirname(pathutils.strip_path(path))
  62. props: Optional[MutableMapping[str, str]] = None
  63. stored_exc_info = None
  64. items = []
  65. try:
  66. if tag and write_whole_collection is not None:
  67. radicale_item.check_and_sanitize_items(
  68. vobject_items, is_collection=write_whole_collection, tag=tag)
  69. if write_whole_collection and tag == "VCALENDAR":
  70. vobject_components: List[vobject.base.Component] = []
  71. vobject_item, = vobject_items
  72. for content in ("vevent", "vtodo", "vjournal"):
  73. vobject_components.extend(
  74. getattr(vobject_item, "%s_list" % content, []))
  75. vobject_components_by_uid = itertools.groupby(
  76. sorted(vobject_components, key=radicale_item.get_uid),
  77. radicale_item.get_uid)
  78. for _, components in vobject_components_by_uid:
  79. vobject_collection = vobject.iCalendar()
  80. for component in components:
  81. vobject_collection.add(component)
  82. vobject_collection.add(vobject.base.ContentLine("PRODID", [], PRODID))
  83. item = radicale_item.Item(collection_path=collection_path,
  84. vobject_item=vobject_collection)
  85. item.prepare()
  86. items.append(item)
  87. elif write_whole_collection and tag == "VADDRESSBOOK":
  88. for vobject_item in vobject_items:
  89. item = radicale_item.Item(collection_path=collection_path,
  90. vobject_item=vobject_item)
  91. item.prepare()
  92. items.append(item)
  93. elif not write_whole_collection:
  94. vobject_item, = vobject_items
  95. item = radicale_item.Item(collection_path=collection_path,
  96. vobject_item=vobject_item)
  97. item.prepare()
  98. items.append(item)
  99. if write_whole_collection:
  100. props = {}
  101. if tag:
  102. props["tag"] = tag
  103. if tag == "VCALENDAR" and vobject_items:
  104. if hasattr(vobject_items[0], "x_wr_calname"):
  105. calname = vobject_items[0].x_wr_calname.value
  106. if calname:
  107. props["D:displayname"] = calname
  108. if hasattr(vobject_items[0], "x_wr_caldesc"):
  109. caldesc = vobject_items[0].x_wr_caldesc.value
  110. if caldesc:
  111. props["C:calendar-description"] = caldesc
  112. props = radicale_item.check_and_sanitize_props(props)
  113. except Exception:
  114. exc_info_or_none_tuple = sys.exc_info()
  115. assert exc_info_or_none_tuple[0] is not None
  116. stored_exc_info = exc_info_or_none_tuple
  117. # Use iterator for items and delete references to free memory early
  118. def items_iter() -> Iterator[radicale_item.Item]:
  119. while items:
  120. yield items.pop(0)
  121. return items_iter(), tag, write_whole_collection, props, stored_exc_info
  122. class ApplicationPartPut(ApplicationBase):
  123. def do_PUT(self, environ: types.WSGIEnviron, base_prefix: str,
  124. path: str, user: str) -> types.WSGIResponse:
  125. """Manage PUT request."""
  126. access = Access(self._rights, user, path)
  127. if not access.check("w"):
  128. return httputils.NOT_ALLOWED
  129. try:
  130. content = httputils.read_request_body(self.configuration, environ)
  131. except RuntimeError as e:
  132. logger.warning("Bad PUT request on %r (read_request_body): %s", path, e, exc_info=True)
  133. return httputils.BAD_REQUEST
  134. except socket.timeout:
  135. logger.debug("Client timed out", exc_info=True)
  136. return httputils.REQUEST_TIMEOUT
  137. # Prepare before locking
  138. content_type = environ.get("CONTENT_TYPE", "").split(";",
  139. maxsplit=1)[0]
  140. try:
  141. vobject_items = radicale_item.read_components(content or "")
  142. except Exception as e:
  143. logger.warning(
  144. "Bad PUT request on %r (read_components): %s", path, e, exc_info=True)
  145. if self._log_bad_put_request_content:
  146. logger.warning("Bad PUT request content of %r:\n%s", path, content)
  147. else:
  148. logger.debug("Bad PUT request content: suppressed by config/option [logging] bad_put_request_content")
  149. return httputils.BAD_REQUEST
  150. (prepared_items, prepared_tag, prepared_write_whole_collection,
  151. prepared_props, prepared_exc_info) = prepare(
  152. vobject_items, path, content_type,
  153. bool(rights.intersect(access.permissions, "Ww")),
  154. bool(rights.intersect(access.parent_permissions, "w")))
  155. with self._storage.acquire_lock("w", user):
  156. item = next(iter(self._storage.discover(path)), None)
  157. parent_item = next(iter(
  158. self._storage.discover(access.parent_path)), None)
  159. if not isinstance(parent_item, storage.BaseCollection):
  160. return httputils.CONFLICT
  161. write_whole_collection = (
  162. isinstance(item, storage.BaseCollection) or
  163. not parent_item.tag)
  164. if write_whole_collection:
  165. tag = prepared_tag
  166. else:
  167. tag = parent_item.tag
  168. if write_whole_collection:
  169. if ("w" if tag else "W") not in access.permissions:
  170. if not parent_item.tag:
  171. logger.warning("Not a collection (check .Radicale.props): %r", parent_item.path)
  172. return httputils.NOT_ALLOWED
  173. if not self._permit_overwrite_collection:
  174. if ("O") not in access.permissions:
  175. logger.info("overwrite of collection is prevented by config/option [rights] permit_overwrite_collection and not explicit allowed by permssion 'O': %r", path)
  176. return httputils.NOT_ALLOWED
  177. else:
  178. if ("o") in access.permissions:
  179. logger.info("overwrite of collection is allowed by config/option [rights] permit_overwrite_collection but explicit forbidden by permission 'o': %r", path)
  180. return httputils.NOT_ALLOWED
  181. elif "w" not in access.parent_permissions:
  182. return httputils.NOT_ALLOWED
  183. etag = environ.get("HTTP_IF_MATCH", "")
  184. if not item and etag:
  185. # Etag asked but no item found: item has been removed
  186. logger.warning("Precondition failed on PUT request for %r (HTTP_IF_MATCH: %s, item not existing)", path, etag)
  187. return httputils.PRECONDITION_FAILED
  188. if item and etag and item.etag != etag:
  189. # Etag asked but item not matching: item has changed
  190. logger.warning("Precondition failed on PUT request for %r (HTTP_IF_MATCH: %s, item has different etag: %s)", path, etag, item.etag)
  191. return httputils.PRECONDITION_FAILED
  192. if item and etag:
  193. logger.debug("Precondition passed on PUT request for %r (HTTP_IF_MATCH: %s, item has etag: %s)", path, etag, item.etag)
  194. match = environ.get("HTTP_IF_NONE_MATCH", "") == "*"
  195. if item and match:
  196. # Creation asked but item found: item can't be replaced
  197. logger.warning("Precondition failed on PUT request for %r (HTTP_IF_NONE_MATCH: *, creation requested but item found with etag: %s)", path, item.etag)
  198. return httputils.PRECONDITION_FAILED
  199. if match:
  200. logger.debug("Precondition passed on PUT request for %r (HTTP_IF_NONE_MATCH: *)", path)
  201. if (tag != prepared_tag or
  202. prepared_write_whole_collection != write_whole_collection):
  203. (prepared_items, prepared_tag, prepared_write_whole_collection,
  204. prepared_props, prepared_exc_info) = prepare(
  205. vobject_items, path, content_type,
  206. bool(rights.intersect(access.permissions, "Ww")),
  207. bool(rights.intersect(access.parent_permissions, "w")),
  208. tag, write_whole_collection)
  209. props = prepared_props
  210. if prepared_exc_info:
  211. logger.warning(
  212. "Bad PUT request on %r (prepare): %s", path, prepared_exc_info[1],
  213. exc_info=prepared_exc_info)
  214. return httputils.BAD_REQUEST
  215. if write_whole_collection:
  216. try:
  217. etag = self._storage.create_collection(
  218. path, prepared_items, props).etag
  219. for item in prepared_items:
  220. hook_notification_item = HookNotificationItem(
  221. HookNotificationItemTypes.UPSERT,
  222. access.path,
  223. item.serialize()
  224. )
  225. self._hook.notify(hook_notification_item)
  226. except ValueError as e:
  227. logger.warning(
  228. "Bad PUT request on %r (create_collection): %s", path, e, exc_info=True)
  229. return httputils.BAD_REQUEST
  230. else:
  231. assert not isinstance(item, storage.BaseCollection)
  232. prepared_item, = prepared_items
  233. if (item and item.uid != prepared_item.uid or
  234. not item and parent_item.has_uid(prepared_item.uid)):
  235. return self._webdav_error_response(
  236. client.CONFLICT, "%s:no-uid-conflict" % (
  237. "C" if tag == "VCALENDAR" else "CR"))
  238. href = posixpath.basename(pathutils.strip_path(path))
  239. try:
  240. etag = parent_item.upload(href, prepared_item).etag
  241. hook_notification_item = HookNotificationItem(
  242. HookNotificationItemTypes.UPSERT,
  243. access.path,
  244. prepared_item.serialize()
  245. )
  246. self._hook.notify(hook_notification_item)
  247. except ValueError as e:
  248. # return better matching HTTP result in case errno is provided and catched
  249. errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e))
  250. if errno_match:
  251. logger.error(
  252. "Failed PUT request on %r (upload): %s", path, e, exc_info=True)
  253. errno_e = int(errno_match.group(1))
  254. if errno_e == errno.ENOSPC:
  255. return httputils.INSUFFICIENT_STORAGE
  256. elif errno_e in [errno.EPERM, errno.EACCES]:
  257. return httputils.FORBIDDEN
  258. else:
  259. return httputils.INTERNAL_SERVER_ERROR
  260. else:
  261. logger.warning(
  262. "Bad PUT request on %r (upload): %s", path, e, exc_info=True)
  263. return httputils.BAD_REQUEST
  264. headers = {"ETag": etag}
  265. return client.CREATED, headers, None