put.py 13 KB

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