put.py 13 KB

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