put.py 11 KB

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